| // 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:convert'; |
| |
| import 'package:collection/collection.dart' hide mapMap; |
| import 'package:path/path.dart' as p; |
| import 'package:pub_semver/pub_semver.dart'; |
| import 'package:source_span/source_span.dart'; |
| import 'package:yaml/yaml.dart'; |
| |
| import 'io.dart'; |
| import 'language_version.dart'; |
| import 'package_config.dart'; |
| import 'package_name.dart'; |
| import 'sdk.dart' show sdk; |
| import 'system_cache.dart'; |
| import 'utils.dart'; |
| |
| /// A parsed and validated `pubspec.lock` file. |
| class LockFile { |
| /// The packages this lockfile pins. |
| final Map<String, PackageId> packages; |
| |
| /// The intersections of all SDK constraints for all locked packages, indexed |
| /// by SDK identifier. |
| Map<String, VersionConstraint> sdkConstraints; |
| |
| /// Dependency names that appeared in the root package's `dependencies` |
| /// section. |
| final Set<String> _mainDependencies; |
| |
| /// Dependency names that appeared in the root package's `dev_dependencies` |
| /// section. |
| final Set<String> _devDependencies; |
| |
| /// Dependency names that appeared in the root package's |
| /// `dependency_overrides` section. |
| final Set<String> _overriddenDependencies; |
| |
| /// Creates a new lockfile containing [ids]. |
| /// |
| /// If passed, [mainDependencies], [devDependencies], and |
| /// [overriddenDependencies] indicate which dependencies should be marked as |
| /// being listed in the main package's `dependencies`, `dev_dependencies`, and |
| /// `dependency_overrides` sections, respectively. These are consumed by the |
| /// analysis server to provide better auto-completion. |
| LockFile(Iterable<PackageId> ids, |
| {Map<String, VersionConstraint>? sdkConstraints, |
| Set<String>? mainDependencies, |
| Set<String>? devDependencies, |
| Set<String>? overriddenDependencies}) |
| : this._( |
| Map.fromIterable(ids.where((id) => !id.isRoot), |
| key: (id) => id.name), |
| sdkConstraints ?? {'dart': VersionConstraint.any}, |
| mainDependencies ?? const UnmodifiableSetView.empty(), |
| devDependencies ?? const UnmodifiableSetView.empty(), |
| overriddenDependencies ?? const UnmodifiableSetView.empty()); |
| |
| LockFile._( |
| Map<String, PackageId> packages, |
| this.sdkConstraints, |
| this._mainDependencies, |
| this._devDependencies, |
| this._overriddenDependencies) |
| : packages = UnmodifiableMapView(packages); |
| |
| LockFile.empty() |
| : packages = const {}, |
| sdkConstraints = {'dart': VersionConstraint.any}, |
| _mainDependencies = const UnmodifiableSetView.empty(), |
| _devDependencies = const UnmodifiableSetView.empty(), |
| _overriddenDependencies = const UnmodifiableSetView.empty(); |
| |
| /// Loads a lockfile from [filePath]. |
| factory LockFile.load(String filePath, SourceRegistry sources) { |
| return LockFile._parse(filePath, readTextFile(filePath), sources); |
| } |
| |
| /// Parses a lockfile whose text is [contents]. |
| /// |
| /// If [filePath] is given, path-dependencies will be interpreted relative to |
| /// that. |
| factory LockFile.parse(String contents, SourceRegistry sources, |
| {String? filePath}) { |
| return LockFile._parse(filePath, contents, sources); |
| } |
| |
| /// Parses the lockfile whose text is [contents]. |
| /// |
| /// [filePath] is the system-native path to the lockfile on disc. It may be |
| /// `null`. |
| static LockFile _parse( |
| String? filePath, String contents, SourceRegistry sources) { |
| if (contents.trim() == '') return LockFile.empty(); |
| |
| Uri? sourceUrl; |
| if (filePath != null) sourceUrl = p.toUri(filePath); |
| final parsed = _parseNode<YamlMap>( |
| loadYamlNode(contents, sourceUrl: sourceUrl), |
| 'YAML mapping', |
| ); |
| |
| final sdkConstraints = <String, VersionConstraint>{}; |
| final sdkNode = |
| _getEntry<YamlScalar?>(parsed, 'sdk', 'string', required: false); |
| if (sdkNode != null) { |
| // Lockfiles produced by pub versions from 1.14.0 through 1.18.0 included |
| // a top-level "sdk" field which encoded the unified constraint on the |
| // Dart SDK. They had no way of specifying constraints on other SDKs. |
| sdkConstraints['dart'] = _parseVersionConstraint(sdkNode); |
| } |
| |
| final sdksField = |
| _getEntry<YamlMap?>(parsed, 'sdks', 'map', required: false); |
| |
| if (sdksField != null) { |
| _parseEachEntry<String, YamlScalar>(sdksField, (name, constraint) { |
| sdkConstraints[name] = _parseVersionConstraint(constraint); |
| }, 'string', 'string'); |
| } |
| |
| final packages = <String, PackageId>{}; |
| |
| final mainDependencies = <String>{}; |
| final devDependencies = <String>{}; |
| final overriddenDependencies = <String>{}; |
| |
| final packageEntries = |
| _getEntry<YamlMap?>(parsed, 'packages', 'map', required: false); |
| |
| if (packageEntries != null) { |
| _parseEachEntry<String, YamlMap>(packageEntries, (name, spec) { |
| // Parse the version. |
| final versionEntry = _getStringEntry(spec, 'version'); |
| final version = Version.parse(versionEntry); |
| |
| // Parse the source. |
| final sourceName = _getStringEntry(spec, 'source'); |
| |
| var descriptionNode = |
| _getEntry<YamlNode>(spec, 'description', 'description'); |
| |
| dynamic description = descriptionNode is YamlScalar |
| ? descriptionNode.value |
| : descriptionNode; |
| |
| // Let the source parse the description. |
| var source = sources(sourceName); |
| PackageId id; |
| try { |
| id = source.parseId(name, version, description, |
| containingDir: filePath == null ? null : p.dirname(filePath)); |
| } on FormatException catch (ex) { |
| _failAt(ex.message, spec.nodes['description']!); |
| } |
| |
| // Validate the name. |
| if (name != id.name) { |
| _failAt("Package name $name doesn't match ${id.name}.", spec); |
| } |
| |
| packages[name] = id; |
| if (spec.containsKey('dependency')) { |
| final dependencyKind = _getStringEntry(spec, 'dependency'); |
| switch (dependencyKind) { |
| case _directMain: |
| mainDependencies.add(name); |
| break; |
| case _directDev: |
| devDependencies.add(name); |
| break; |
| case _directOverridden: |
| overriddenDependencies.add(name); |
| } |
| } |
| }, 'string', 'map'); |
| } |
| return LockFile._( |
| packages, |
| sdkConstraints, |
| const UnmodifiableSetView.empty(), |
| const UnmodifiableSetView.empty(), |
| const UnmodifiableSetView.empty()); |
| } |
| |
| /// Runs [fn] and wraps any [FormatException] it throws in a |
| /// [SourceSpanFormatException]. |
| /// |
| /// [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. |
| static T _wrapFormatException<T>( |
| String description, SourceSpan span, T Function() fn) { |
| try { |
| return fn(); |
| } on FormatException catch (e) { |
| throw SourceSpanFormatException( |
| 'Invalid $description: ${e.message}', span); |
| } |
| } |
| |
| static VersionConstraint _parseVersionConstraint(YamlNode node) { |
| return _parseNode(node, 'version constraint', |
| parse: VersionConstraint.parse); |
| } |
| |
| static String _getStringEntry(YamlMap map, String key) { |
| return _parseNode<String>( |
| _getEntry<YamlScalar>(map, key, 'string'), 'string'); |
| } |
| |
| static T _parseNode<T>(YamlNode node, String typeDescription, |
| {T Function(String)? parse}) { |
| if (node is T) { |
| return node as T; |
| } else if (node is YamlScalar) { |
| final value = node.value; |
| if (parse != null) { |
| if (value is! String) { |
| _failAt('Expected a $typeDescription.', node); |
| } |
| return _wrapFormatException( |
| 'Expected a $typeDescription.', node.span, () => parse(node.value)); |
| } else if (value is T) { |
| return value; |
| } |
| _failAt('Expected a $typeDescription.', node); |
| } |
| _failAt('Expected a $typeDescription.', node); |
| } |
| |
| static void _parseEachEntry<K, V>( |
| YamlMap map, |
| void Function(K key, V value) f, |
| String keyTypeDescription, |
| String valueTypeDescription) { |
| map.nodes.forEach((key, value) { |
| f(_parseNode(key, keyTypeDescription), |
| _parseNode(value, valueTypeDescription)); |
| }); |
| } |
| |
| static T _getEntry<T>( |
| YamlMap map, |
| String key, |
| String type, { |
| bool required = true, |
| }) { |
| final entry = map.nodes[key]; |
| // `null` here always means not present. A value explicitly mapped to `null` |
| // would be a `YamlScalar(null)`. |
| if (entry == null) { |
| if (required) { |
| _failAt('Expected a `$key` entry.', map); |
| } else { |
| return null as T; |
| } |
| } |
| return _parseNode(entry, type); |
| } |
| |
| static Never _failAt(String message, YamlNode node) { |
| throw SourceSpanFormatException(message, node.span); |
| } |
| |
| /// Returns a copy of this LockFile with a package named [name] removed. |
| /// |
| /// Returns an identical [LockFile] if there's no package named [name]. |
| LockFile removePackage(String name) { |
| if (!this.packages.containsKey(name)) return this; |
| |
| var packages = Map<String, PackageId>.from(this.packages); |
| packages.remove(name); |
| return LockFile._(packages, sdkConstraints, _mainDependencies, |
| _devDependencies, _overriddenDependencies); |
| } |
| |
| /// Returns the contents of the `.dart_tool/package_config` file generated |
| /// from this lockfile. |
| /// |
| /// This file will replace the `.packages` file. |
| /// |
| /// If [entrypoint] is passed, an accompanying [entrypointSdkConstraint] |
| /// should be given, these identify the current package in which this file is |
| /// written. Passing `null` as [entrypointSdkConstraint] is correct if the |
| /// current package has no SDK constraint. |
| Future<String> packageConfigFile( |
| SystemCache cache, { |
| String? entrypoint, |
| VersionConstraint? entrypointSdkConstraint, |
| String? relativeFrom, |
| }) async { |
| final entries = <PackageConfigEntry>[]; |
| for (final name in ordered(packages.keys)) { |
| final id = packages[name]!; |
| final rootPath = cache.getDirectory(id, relativeFrom: relativeFrom); |
| Uri rootUri; |
| if (p.isRelative(rootPath)) { |
| // Relative paths are relative to the root project, we want them |
| // relative to the `.dart_tool/package_config.json` file. |
| rootUri = p.toUri(p.join('..', rootPath)); |
| } else { |
| rootUri = p.toUri(rootPath); |
| } |
| final pubspec = await cache.describe(id); |
| entries.add(PackageConfigEntry( |
| name: name, |
| rootUri: rootUri, |
| packageUri: p.toUri('lib/'), |
| languageVersion: pubspec.languageVersion, |
| )); |
| } |
| |
| if (entrypoint != null) { |
| entries.add(PackageConfigEntry( |
| name: entrypoint, |
| rootUri: p.toUri('../'), |
| packageUri: p.toUri('lib/'), |
| languageVersion: LanguageVersion.fromSdkConstraint( |
| entrypointSdkConstraint, |
| ), |
| )); |
| } |
| |
| final packageConfig = PackageConfig( |
| configVersion: 2, |
| packages: entries, |
| generated: DateTime.now(), |
| generator: 'pub', |
| generatorVersion: sdk.version, |
| ); |
| |
| return '${JsonEncoder.withIndent(' ').convert(packageConfig.toJson())}\n'; |
| } |
| |
| /// Returns the serialized YAML text of the lock file. |
| /// |
| /// [packageDir] is the containing directory of the root package, used to |
| /// serialize relative path package descriptions. If it is null, they will be |
| /// serialized as absolute. |
| String serialize(String? packageDir) { |
| // Convert the dependencies to a simple object. |
| var packageMap = {}; |
| packages.forEach((name, package) { |
| var description = |
| package.description.serializeForLockfile(containingDir: packageDir); |
| |
| packageMap[name] = { |
| 'version': package.version.toString(), |
| 'source': package.source.name, |
| 'description': description, |
| 'dependency': _dependencyType(package.name) |
| }; |
| }); |
| |
| var data = { |
| 'sdks': mapMap(sdkConstraints, |
| value: (_, constraint) => constraint.toString()), |
| 'packages': packageMap |
| }; |
| return ''' |
| # Generated by pub |
| # See https://dart.dev/tools/pub/glossary#lockfile |
| ${yamlToString(data)} |
| '''; |
| } |
| |
| static const _directMain = 'direct main'; |
| static const _directDev = 'direct dev'; |
| static const _directOverridden = 'direct overridden'; |
| static const _transitive = 'transitive'; |
| |
| /// Returns the dependency classification for [package]. |
| String _dependencyType(String package) { |
| if (_mainDependencies.contains(package)) return _directMain; |
| if (_devDependencies.contains(package)) return _directDev; |
| |
| // If a package appears in `dependency_overrides` and another dependency |
| // section, the main section it appears in takes precedence. |
| if (_overriddenDependencies.contains(package)) { |
| return _directOverridden; |
| } |
| return _transitive; |
| } |
| |
| /// `true` if [other] has the same packages as `this` in the same versions |
| /// from the same sources. |
| bool samePackageIds(LockFile other) { |
| if (packages.length != other.packages.length) { |
| return false; |
| } |
| for (final id in packages.values) { |
| final otherId = other.packages[id.name]; |
| if (id != otherId) return false; |
| } |
| return true; |
| } |
| } |