forked from dart-lang/pub
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathpubspec.dart
637 lines (555 loc) · 22.3 KB
/
pubspec.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
// Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
import 'dart:io';
import 'package:collection/collection.dart' hide mapMap;
import 'package:path/path.dart' as path;
import 'package:pub_semver/pub_semver.dart';
import 'package:source_span/source_span.dart';
import 'package:yaml/yaml.dart';
import 'exceptions.dart';
import 'feature.dart';
import 'io.dart';
import 'language_version.dart';
import 'log.dart';
import 'package_name.dart';
import 'pubspec_parse.dart';
import 'sdk.dart';
import 'source_registry.dart';
import 'utils.dart';
export 'pubspec_parse.dart' hide PubspecBase;
/// The default SDK upper bound constraint for packages that don't declare one.
///
/// This provides a sane default for packages that don't have an upper bound.
final VersionRange _defaultUpperBoundSdkConstraint =
VersionConstraint.parse('<2.0.0') as VersionRange;
/// Whether or not to allow the pre-release SDK for packages that have an
/// upper bound Dart SDK constraint of <2.0.0.
///
/// If enabled then a Dart SDK upper bound of <2.0.0 is always converted to
/// <2.0.0-dev.infinity.
///
/// This has a default value of `true` but can be overridden with the
/// PUB_ALLOW_PRERELEASE_SDK system environment variable.
bool get _allowPreReleaseSdk => _allowPreReleaseSdkValue != 'false';
/// The value of the PUB_ALLOW_PRERELEASE_SDK environment variable, defaulted
/// to `true`.
final String _allowPreReleaseSdkValue = () {
var value =
Platform.environment['PUB_ALLOW_PRERELEASE_SDK']?.toLowerCase() ?? 'true';
if (!['true', 'quiet', 'false'].contains(value)) {
warning(yellow('''
The environment variable PUB_ALLOW_PRERELEASE_SDK is set as `$value`.
The expected value is either `true`, `quiet` (true but no logging), or `false`.
Using a default value of `true`.
'''));
value = 'true';
}
return value;
}();
/// Whether or not to warn about pre-release SDK overrides.
bool get warnAboutPreReleaseSdkOverrides => _allowPreReleaseSdkValue != 'quiet';
/// The parsed contents of a pubspec file.
///
/// The fields of a pubspec are, for the most part, validated when they're first
/// accessed. This allows a partially-invalid pubspec to be used if only the
/// valid portions are relevant. To get a list of all errors in the pubspec, use
/// [allErrors].
class Pubspec extends PubspecBase {
// If a new lazily-initialized field is added to this class and the
// initialization can throw a [PubspecException], that error should also be
// exposed through [allErrors].
/// The registry of sources to use when parsing [dependencies] and
/// [devDependencies].
///
/// This will be null if this was created using [new Pubspec] or [new
/// Pubspec.empty].
final SourceRegistry? _sources;
/// The location from which the pubspec was loaded.
///
/// This can be null if the pubspec was created in-memory or if its location
/// is unknown.
Uri? get _location => fields.span.sourceUrl;
/// The additional packages this package depends on.
Map<String, PackageRange> get dependencies => _dependencies ??=
_parseDependencies('dependencies', fields.nodes['dependencies']);
Map<String, PackageRange>? _dependencies;
/// The packages this package depends on when it is the root package.
Map<String, PackageRange> get devDependencies => _devDependencies ??=
_parseDependencies('dev_dependencies', fields.nodes['dev_dependencies']);
Map<String, PackageRange>? _devDependencies;
/// The dependency constraints that this package overrides when it is the
/// root package.
///
/// Dependencies here will replace any dependency on a package with the same
/// name anywhere in the dependency graph.
Map<String, PackageRange> get dependencyOverrides =>
_dependencyOverrides ??= _parseDependencies(
'dependency_overrides', fields.nodes['dependency_overrides']);
Map<String, PackageRange>? _dependencyOverrides;
late final Map<String, Feature> features = _computeFeatures();
Map<String, Feature> _computeFeatures() {
final features = fields['features'];
if (features == null) {
return const {};
}
if (features is! YamlMap) {
_error('"features" field must be a map.', fields.nodes['features']!.span);
}
return mapMap(features.nodes,
key: (dynamic nameNode, dynamic _) => _validateFeatureName(nameNode),
value: (dynamic nameNode, dynamic specNode) {
if (specNode.value == null) {
return Feature(nameNode.value, const []);
}
if (specNode is! YamlMap) {
_error('A feature specification must be a map.', specNode.span);
}
var onByDefault = specNode['default'] ?? true;
if (onByDefault is! bool) {
_error('Default must be true or false.',
specNode.nodes['default']!.span);
}
var requires = _parseStringList(specNode.nodes['requires'],
validate: (name, span) {
if (!features.containsKey(name)) _error('Undefined feature.', span);
});
var dependencies = _parseDependencies(
'dependencies', specNode.nodes['dependencies']);
var sdkConstraints = _parseEnvironment(specNode);
return Feature(nameNode.value, dependencies.values,
requires: requires,
sdkConstraints: sdkConstraints,
onByDefault: onByDefault);
});
}
/// A map from SDK identifiers to constraints on those SDK versions.
Map<String, VersionConstraint> get sdkConstraints {
_ensureEnvironment();
return _sdkConstraints!;
}
Map<String, VersionConstraint>? _sdkConstraints;
/// Whether or not to apply the [_defaultUpperBoundsSdkConstraint] to this
/// pubspec.
final bool _includeDefaultSdkConstraint;
/// Whether or not the SDK version was overridden from <2.0.0 to
/// <2.0.0-dev.infinity.
bool get dartSdkWasOverridden => _dartSdkWasOverridden;
bool _dartSdkWasOverridden = false;
/// The original Dart SDK constraint as written in the pubspec.
///
/// If [dartSdkWasOverridden] is `false`, this will be identical to
/// `sdkConstraints["dart"]`.
VersionConstraint get originalDartSdkConstraint {
_ensureEnvironment();
return _originalDartSdkConstraint ?? sdkConstraints['dart']!;
}
VersionConstraint? _originalDartSdkConstraint;
/// Ensures that the top-level "environment" field has been parsed and
/// [_sdkConstraints] is set accordingly.
void _ensureEnvironment() {
if (_sdkConstraints != null) return;
var sdkConstraints = _parseEnvironment(fields);
var parsedDartSdkConstraint = sdkConstraints['dart'];
if (parsedDartSdkConstraint is VersionRange &&
_shouldEnableCurrentSdk(parsedDartSdkConstraint)) {
_originalDartSdkConstraint = parsedDartSdkConstraint;
_dartSdkWasOverridden = true;
sdkConstraints['dart'] = VersionRange(
min: parsedDartSdkConstraint.min,
includeMin: parsedDartSdkConstraint.includeMin,
max: sdk.version,
includeMax: true);
}
_sdkConstraints = UnmodifiableMapView(sdkConstraints);
}
/// Whether or not we should override [sdkConstraint] to be <= the user's
/// current SDK version.
///
/// This is true if the following conditions are met:
///
/// - [_allowPreReleaseSdk] is `true`
/// - The user's current SDK is a pre-release version.
/// - The original [sdkConstraint] max version is exclusive (`includeMax`
/// is `false`).
/// - The original [sdkConstraint] is not a pre-release version.
/// - The original [sdkConstraint] matches the exact same major, minor, and
/// patch versions as the user's current SDK.
bool _shouldEnableCurrentSdk(VersionRange sdkConstraint) {
if (!_allowPreReleaseSdk) return false;
if (!sdk.version.isPreRelease) return false;
if (sdkConstraint.includeMax) return false;
var minSdkConstraint = sdkConstraint.min;
if (minSdkConstraint != null &&
minSdkConstraint.isPreRelease &&
equalsIgnoringPreRelease(sdkConstraint.min!, sdk.version)) {
return false;
}
var maxSdkConstraint = sdkConstraint.max;
if (maxSdkConstraint == null) return false;
if (maxSdkConstraint.max.isPreRelease &&
!maxSdkConstraint.isFirstPreRelease) {
return false;
}
return equalsIgnoringPreRelease(maxSdkConstraint, sdk.version);
}
/// Parses the "environment" field in [parent] and returns a map from SDK
/// identifiers to constraints on those SDKs.
Map<String, VersionConstraint> _parseEnvironment(YamlMap parent) {
var yaml = parent['environment'];
if (yaml == null) {
return {
'dart': _includeDefaultSdkConstraint
? _defaultUpperBoundSdkConstraint
: VersionConstraint.any
};
}
if (yaml is! YamlMap) {
_error('"environment" field must be a map.',
parent.nodes['environment']!.span);
}
var constraints = {
'dart': _parseVersionConstraint(yaml.nodes['sdk'],
defaultUpperBoundConstraint: _includeDefaultSdkConstraint
? _defaultUpperBoundSdkConstraint
: null)
};
yaml.nodes.forEach((name, constraint) {
if (name.value is! String) {
_error('SDK names must be strings.', name.span);
} else if (name.value == 'dart') {
_error('Use "sdk" to for Dart SDK constraints.', name.span);
}
if (name.value == 'sdk') return;
constraints[name.value as String] = _parseVersionConstraint(constraint,
// Flutter constraints get special treatment, as Flutter won't be
// using semantic versioning to mark breaking releases.
ignoreUpperBound: name.value == 'flutter');
});
return constraints;
}
/// The language version implied by the sdk constraint.
LanguageVersion get languageVersion =>
LanguageVersion.fromSdkConstraint(originalDartSdkConstraint);
/// Loads the pubspec for a package located in [packageDir].
///
/// If [expectedName] is passed and the pubspec doesn't have a matching name
/// field, this will throw a [PubspecException].
factory Pubspec.load(String packageDir, SourceRegistry sources,
{String? expectedName}) {
var pubspecPath = path.join(packageDir, 'pubspec.yaml');
var pubspecUri = path.toUri(pubspecPath);
if (!fileExists(pubspecPath)) {
throw FileException(
// Make the package dir absolute because for the entrypoint it'll just
// be ".", which may be confusing.
'Could not find a file named "pubspec.yaml" in '
'"${canonicalize(packageDir)}".',
pubspecPath);
}
return Pubspec.parse(readTextFile(pubspecPath), sources,
expectedName: expectedName, location: pubspecUri);
}
Pubspec(String name,
{Version? version,
Iterable<PackageRange>? dependencies,
Iterable<PackageRange>? devDependencies,
Iterable<PackageRange>? dependencyOverrides,
Map? fields,
SourceRegistry? sources,
Map<String, VersionConstraint>? sdkConstraints})
: _dependencies = dependencies == null
? null
: Map.fromIterable(dependencies, key: (range) => range.name),
_devDependencies = devDependencies == null
? null
: Map.fromIterable(devDependencies, key: (range) => range.name),
_dependencyOverrides = dependencyOverrides == null
? null
: Map.fromIterable(dependencyOverrides, key: (range) => range.name),
_sdkConstraints = sdkConstraints ??
UnmodifiableMapView({'dart': VersionConstraint.any}),
_includeDefaultSdkConstraint = false,
_sources = sources,
super(
fields == null ? YamlMap() : YamlMap.wrap(fields),
name: name,
version: version,
);
Pubspec.empty()
: _sources = null,
_dependencies = {},
_devDependencies = {},
_sdkConstraints = {'dart': VersionConstraint.any},
_includeDefaultSdkConstraint = false,
super(
YamlMap(),
version: Version.none,
);
/// Returns a Pubspec object for an already-parsed map representing its
/// contents.
///
/// If [expectedName] is passed and the pubspec doesn't have a matching name
/// field, this will throw a [PubspecError].
///
/// [location] is the location from which this pubspec was loaded.
Pubspec.fromMap(Map fields, this._sources,
{String? expectedName, Uri? location})
: _includeDefaultSdkConstraint = true,
super(fields is YamlMap
? fields
: YamlMap.wrap(fields, sourceUrl: location)) {
// If [expectedName] is passed, ensure that the actual 'name' field exists
// and matches the expectation.
if (expectedName == null) return;
if (name == expectedName) return;
throw PubspecException(
'"name" field doesn\'t match expected name '
'"$expectedName".',
this.fields.nodes['name']!.span);
}
/// Parses the pubspec stored at [filePath] whose text is [contents].
///
/// If the pubspec doesn't define a version for itself, it defaults to
/// [Version.none].
factory Pubspec.parse(String contents, SourceRegistry sources,
{String? expectedName, Uri? location}) {
YamlNode pubspecNode;
try {
pubspecNode = loadYamlNode(contents, sourceUrl: location);
} on YamlException catch (error) {
throw PubspecException(error.message, error.span);
}
Map pubspecMap;
if (pubspecNode is YamlScalar && pubspecNode.value == null) {
pubspecMap = YamlMap(sourceUrl: location);
} else if (pubspecNode is YamlMap) {
pubspecMap = pubspecNode;
} else {
throw PubspecException(
'The pubspec must be a YAML mapping.', pubspecNode.span);
}
return Pubspec.fromMap(pubspecMap, sources,
expectedName: expectedName, location: location);
}
/// Returns a list of most errors in this pubspec.
///
/// This will return at most one error for each field.
List<PubspecException> get allErrors {
var errors = <PubspecException>[];
void _collectError(void Function() fn) {
try {
fn();
} on PubspecException catch (e) {
errors.add(e);
}
}
_collectError(() => name);
_collectError(() => version);
_collectError(() => dependencies);
_collectError(() => devDependencies);
_collectError(() => publishTo);
_collectError(() => features);
_collectError(() => executables);
_collectError(() => falseSecrets);
_collectError(_ensureEnvironment);
return errors;
}
/// Parses the dependency field named [field], and returns the corresponding
/// map of dependency names to dependencies.
Map<String, PackageRange> _parseDependencies(String field, YamlNode? node) {
var dependencies = <String, PackageRange>{};
// Allow an empty dependencies key.
if (node == null || node.value == null) return dependencies;
if (node is! YamlMap) {
_error('"$field" field must be a map.', node.span);
}
var nonStringNode = node.nodes.keys
.firstWhere((e) => e.value is! String, orElse: () => null);
if (nonStringNode != null) {
_error('A dependency name must be a string.', nonStringNode.span);
}
node.nodes.forEach((nameNode, specNode) {
var name = nameNode.value;
var spec = specNode.value;
if (fields['name'] != null && name == this.name) {
_error('A package may not list itself as a dependency.', nameNode.span);
}
YamlNode? descriptionNode;
String? sourceName;
VersionConstraint versionConstraint = VersionRange();
var features = const <String, FeatureDependency>{};
if (spec == null) {
sourceName = _sources!.defaultSource.name;
} else if (spec is String) {
sourceName = _sources!.defaultSource.name;
versionConstraint = _parseVersionConstraint(specNode);
} else if (spec is Map) {
// Don't write to the immutable YAML map.
spec = Map.from(spec);
var specMap = specNode as YamlMap;
if (spec.containsKey('version')) {
spec.remove('version');
versionConstraint = _parseVersionConstraint(specMap.nodes['version']);
}
if (spec.containsKey('features')) {
spec.remove('features');
features = _parseDependencyFeatures(specMap.nodes['features']);
}
var sourceNames = spec.keys.toList();
if (sourceNames.length > 1) {
_error('A dependency may only have one source.', specNode.span);
} else if (sourceNames.isEmpty) {
// Default to a hosted dependency if no source is specified.
sourceName = 'hosted';
}
sourceName ??= sourceNames.single;
if (sourceName is! String) {
_error('A source name must be a string.',
specMap.nodes.keys.single.span);
}
descriptionNode ??= specMap.nodes[sourceName];
} else {
_error('A dependency specification must be a string or a mapping.',
specNode.span);
}
// Let the source validate the description.
var ref = _wrapFormatException('description', descriptionNode?.span, () {
String? pubspecPath;
var location = _location;
if (location != null && _isFileUri(location)) {
pubspecPath = path.fromUri(_location);
}
return _sources![sourceName]!.parseRef(
name,
descriptionNode?.value,
containingPath: pubspecPath,
languageVersion: languageVersion,
);
}, targetPackage: name);
dependencies[name] =
ref.withConstraint(versionConstraint).withFeatures(features);
});
return dependencies;
}
/// Parses [node] to a [VersionConstraint].
///
/// If or [defaultUpperBoundConstraint] is specified then it will be set as
/// the max constraint if the original constraint doesn't have an upper
/// bound and it is compatible with [defaultUpperBoundConstraint].
///
/// If [ignoreUpperBound] the max constraint is ignored.
VersionConstraint _parseVersionConstraint(YamlNode? node,
{VersionConstraint? defaultUpperBoundConstraint,
bool ignoreUpperBound = false}) {
if (node?.value == null) {
return defaultUpperBoundConstraint ?? VersionConstraint.any;
}
if (node!.value is! String) {
_error('A version constraint must be a string.', node.span);
}
return _wrapFormatException('version constraint', node.span, () {
var constraint = VersionConstraint.parse(node.value);
if (defaultUpperBoundConstraint != null &&
constraint is VersionRange &&
constraint.max == null &&
defaultUpperBoundConstraint.allowsAny(constraint)) {
constraint = VersionConstraint.intersection(
[constraint, defaultUpperBoundConstraint]);
}
if (ignoreUpperBound && constraint is VersionRange) {
return VersionRange(
min: constraint.min, includeMin: constraint.includeMin);
}
return constraint;
});
}
/// Parses [node] to a map from feature names to whether those features are
/// enabled.
Map<String, FeatureDependency> _parseDependencyFeatures(YamlNode? node) {
if (node?.value == null) return const {};
if (node is! YamlMap) _error('Features must be a map.', node!.span);
return mapMap(node.nodes,
key: (dynamic nameNode, dynamic _) => _validateFeatureName(nameNode),
value: (dynamic _, dynamic valueNode) {
var value = valueNode.value;
if (value is bool) {
return value
? FeatureDependency.required
: FeatureDependency.unused;
} else if (value is String && value == 'if available') {
return FeatureDependency.ifAvailable;
} else {
_error('Features must be true, false, or "if available".',
valueNode.span);
}
});
}
/// Verifies that [node] is a string and a valid feature name, and returns it
/// if so.
String _validateFeatureName(YamlNode node) {
var name = node.value;
if (name is! String) {
_error('A feature name must be a string.', node.span);
} else if (!packageNameRegExp.hasMatch(name)) {
_error('A feature name must be a valid Dart identifier.', node.span);
}
return name;
}
/// Verifies that [node] is a list of strings and returns it.
///
/// If [validate] is passed, it's called for each string in [node].
List<String> _parseStringList(YamlNode? node,
{void Function(String value, SourceSpan)? validate}) {
var list = _parseList(node);
for (var element in list.nodes) {
var value = element.value;
if (value is String) {
if (validate != null) validate(value, element.span);
} else {
_error('Must be a string.', element.span);
}
}
return list.cast<String>();
}
/// Verifies that [node] is a list and returns it.
YamlList _parseList(YamlNode? node) {
if (node == null || node.value == null) return YamlList();
if (node is YamlList) return node;
_error('Must be a list.', node.span);
}
/// Runs [fn] and wraps any [FormatException] it throws in a
/// [PubspecException].
///
/// [description] should be a noun phrase that describes whatever's being
/// parsed or processed by [fn]. [span] should be the location of whatever's
/// being processed within the pubspec.
///
/// If [targetPackage] is provided, the value is used to describe the
/// dependency that caused the problem.
T _wrapFormatException<T>(
String description, SourceSpan? span, T Function() fn,
{String? targetPackage}) {
try {
return fn();
} on FormatException catch (e) {
// If we already have a pub exception with a span, re-use that
if (e is PubspecException) rethrow;
var msg = 'Invalid $description';
if (targetPackage != null) {
msg = '$msg in the "$name" pubspec on the "$targetPackage" dependency';
}
msg = '$msg: ${e.message}';
_error(msg, span);
}
}
/// Throws a [PubspecException] with the given message.
Never _error(String message, SourceSpan? span) {
throw PubspecException(message, span);
}
}
/// Returns whether [uri] is a file URI.
///
/// This is slightly more complicated than just checking if the scheme is
/// 'file', since relative URIs also refer to the filesystem on the VM.
bool _isFileUri(Uri uri) => uri.scheme == 'file' || uri.scheme == '';