Merge pull request #2238 from sigurdm/language_versioning
Language versioning
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index 3e5ecd1..ebe75f5 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -3,11 +3,13 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:async';
+import 'dart:convert';
import 'dart:io';
import 'package:collection/collection.dart';
import 'package:package_config/packages_file.dart' as packages_file;
import 'package:path/path.dart' as p;
+import 'package:pub/src/package_config.dart';
import 'package:pub_semver/pub_semver.dart';
import 'dart.dart' as dart;
@@ -17,6 +19,7 @@
import 'lock_file.dart';
import 'log.dart' as log;
import 'package.dart';
+import 'package_config.dart' show PackageConfig;
import 'package_graph.dart';
import 'package_name.dart';
import 'pubspec.dart';
@@ -125,6 +128,10 @@
/// The path to the entrypoint's ".packages" file.
String get packagesFile => root.path('.packages');
+ /// The path to the entrypoint's ".dart_tool/package_config.json" file.
+ String get packageConfigFile =>
+ root.path('.dart_tool', 'package_config.json');
+
/// The path to the entrypoint package's pubspec.
String get pubspecPath => root.path('pubspec.yaml');
@@ -142,12 +149,6 @@
return newPath;
}
- /// Returns the contents of the `.packages` file for this entrypoint.
- ///
- /// This is based on the package's lockfile, so it works whether or not a
- /// `.packages` file has been written.
- String get packagesFileContents => lockFile.packagesFile(cache, root.name);
-
/// The path to the directory containing dependency executable snapshots.
String get _snapshotPath => p.join(cachePath, 'bin');
@@ -177,6 +178,19 @@
_lockFile = _packageGraph.lockFile;
}
+ /// Writes .packages and .dart_tool/package_config.json
+ Future<void> writePackagesFiles() async {
+ writeTextFile(packagesFile, lockFile.packagesFile(cache, root.name));
+ ensureDir(p.dirname(packageConfigFile));
+ writeTextFile(
+ packageConfigFile,
+ await lockFile.packageConfigFile(
+ cache,
+ entrypoint: root.name,
+ entrypointSdkConstraint: root.pubspec.sdkConstraints[sdk.identifier],
+ ));
+ }
+
/// Gets all dependencies of the [root] package.
///
/// Performs version resolution according to [SolveType].
@@ -237,7 +251,7 @@
/// have to reload and reparse all the pubspecs.
_packageGraph = PackageGraph.fromSolveResult(this, result);
- writeTextFile(packagesFile, packagesFileContents);
+ await writePackagesFiles();
try {
if (precompile) {
@@ -375,8 +389,9 @@
});
}
- /// Throws a [DataError] if the `.packages` file doesn't exist or if it's
- /// out-of-date relative to the lockfile or the pubspec.
+ /// Throws a [DataError] if the `.packages` file or the
+ /// `.dart_tool/package_config.json` file doesn't exist or if it's out-of-date
+ /// relative to the lockfile or the pubspec.
void assertUpToDate() {
if (_inMemory) return;
@@ -388,6 +403,12 @@
dataError('No .packages file found, please run "pub get" first.');
}
+ if (!entryExists(packageConfigFile)) {
+ dataError(
+ 'No .dart_tool/package_config.json file found, please run "pub get" first.',
+ );
+ }
+
// Manually parse the lockfile because a full YAML parse is relatively slow
// and this is on the hot path for "pub run".
var lockFileText = readTextFile(lockFilePath);
@@ -410,16 +431,21 @@
var packagesModified = File(packagesFile).lastModifiedSync();
if (packagesModified.isBefore(lockFileModified)) {
- if (_isPackagesFileUpToDate()) {
- touch(packagesFile);
- } else {
- dataError('The pubspec.lock file has changed since the .packages file '
- 'was generated, please run "pub get" again.');
- }
+ _checkPackagesFileUpToDate();
+ touch(packagesFile);
} else if (touchedLockFile) {
touch(packagesFile);
}
+ var packageConfigModified = File(packageConfigFile).lastModifiedSync();
+ if (packageConfigModified.isBefore(lockFileModified) ||
+ hasPathDependencies) {
+ _checkPackageConfigUpToDate();
+ touch(packageConfigFile);
+ } else if (touchedLockFile) {
+ touch(packageConfigFile);
+ }
+
for (var match in _sdkConstraint.allMatches(lockFileText)) {
var identifier = match[1] == 'sdk' ? 'dart' : match[1].trim();
var sdk = sdks[identifier];
@@ -500,50 +526,159 @@
});
}
- /// Determines whether or not the `.packages` file is out of date with respect
- /// to the lockfile.
+ /// Determines [lockfile] agrees with the given [packagePathsMapping].
///
- /// This will be `false` if the packages file contains dependencies that are
- /// not in the lockfile or that don't match what's in there.
- bool _isPackagesFileUpToDate() {
+ /// The [packagePathsMapping] is a mapping from package names to paths where
+ /// the packages are located. (The library is located under
+ /// `lib/` relative to the path given).
+ bool _isPackagePathsMappingUptodateWithLockfile(
+ Map<String, String> packagePathsMapping) =>
+ lockFile.packages.values.every((lockFileId) {
+ // It's very unlikely that the lockfile is invalid here, but it's not
+ // impossible—for example, the user may have a very old application
+ // package with a checked-in lockfile that's newer than the pubspec, but
+ // that contains SDK dependencies.
+ if (lockFileId.source is UnknownSource) return false;
+
+ final packagePath = packagePathsMapping[lockFileId.name];
+ if (packagePath == null) {
+ return false;
+ }
+
+ final source = cache.source(lockFileId.source);
+ final lockFilePackagePath =
+ p.join(root.dir, source.getDirectory(lockFileId));
+
+ // Make sure that the packagePath agrees with the lock file about the
+ // path to the package.
+ if (p.normalize(packagePath) != p.normalize(lockFilePackagePath)) {
+ return false;
+ }
+
+ // For cached sources, make sure the directory exists and looks like a
+ // package. This is also done by [_arePackagesAvailable] but that may not
+ // be run if the lockfile is newer than the pubspec.
+ if (source is CachedSource && !dirExists(lockFilePackagePath) ||
+ !fileExists(p.join(lockFilePackagePath, "pubspec.yaml"))) {
+ return false;
+ }
+
+ return true;
+ });
+
+ /// Checks whether or not the `.packages` file is out of date with respect
+ /// to the [lockfile].
+ ///
+ /// This will throw a [DataError] if [lockfile] contains dependencies that
+ /// are not in the `.packages` or that don't match what's in there.
+ void _checkPackagesFileUpToDate() {
+ void outOfDate() {
+ dataError('The pubspec.lock file has changed since the .packages file '
+ 'was generated, please run "pub get" again.');
+ }
+
var packages = packages_file.parse(
File(packagesFile).readAsBytesSync(), p.toUri(packagesFile));
- return lockFile.packages.values.every((lockFileId) {
- // It's very unlikely that the lockfile is invalid here, but it's not
- // impossible—for example, the user may have a very old application
- // package with a checked-in lockfile that's newer than the pubspec, but
- // that contains SDK dependencies.
- if (lockFileId.source is UnknownSource) return false;
-
- var packagesFileUri = packages[lockFileId.name];
- if (packagesFileUri == null) return false;
+ final packagePathsMapping = <String, String>{};
+ for (final package in packages.keys) {
+ final packageUri = packages[package];
// Pub only generates "file:" and relative URIs.
- if (packagesFileUri.scheme != 'file' &&
- packagesFileUri.scheme.isNotEmpty) {
- return false;
+ if (packageUri.scheme != 'file' && packageUri.scheme.isNotEmpty) {
+ outOfDate();
}
- var source = cache.source(lockFileId.source);
-
// Get the dirname of the .packages path, since it's pointing to lib/.
- var packagesFilePath =
- p.dirname(p.join(root.dir, p.fromUri(packagesFileUri)));
- var lockFilePath = p.join(root.dir, source.getDirectory(lockFileId));
+ final packagePath = p.dirname(p.join(root.dir, p.fromUri(packageUri)));
+ packagePathsMapping[package] = packagePath;
+ }
- // For cached sources, make sure the directory exists and looks like a
- // package. This is also done by [_arePackagesAvailable] but that may not
- // be run if the lockfile is newer than the pubspec.
- if (source is CachedSource && !dirExists(packagesFilePath) ||
- !fileExists(p.join(packagesFilePath, "pubspec.yaml"))) {
- return false;
+ if (!_isPackagePathsMappingUptodateWithLockfile(packagePathsMapping)) {
+ outOfDate();
+ }
+ }
+
+ /// Checks whether or not the `.dart_tool/package_config.json` file is
+ /// out of date with respect to the lockfile.
+ ///
+ /// This will throw a [DataError] if the [lockfile] contains dependencies that
+ /// are not in the `.dart_tool/package_config.json` or that don't match
+ /// what's in there.
+ ///
+ /// Throws [DataException], if `.dart_tool/package_config.json` is not
+ /// up-to-date for some other reason.
+ void _checkPackageConfigUpToDate() {
+ void outOfDate() {
+ dataError('The pubspec.lock file has changed since the '
+ '.dart_tool/package_config.json file '
+ 'was generated, please run "pub get" again.');
+ }
+
+ void badPackageConfig() {
+ dataError(
+ 'The ".dart_tool/package_config.json" file is not recognized by '
+ '"pub" version, please run "pub get".');
+ }
+
+ String packageConfigRaw;
+ try {
+ packageConfigRaw = readTextFile(packageConfigFile);
+ } on FileException {
+ dataError(
+ 'The ".dart_tool/package_config.json" file does not exist, please run "pub get".');
+ }
+
+ PackageConfig cfg;
+ try {
+ cfg = PackageConfig.fromJson(json.decode(packageConfigRaw));
+ } on FormatException {
+ badPackageConfig();
+ }
+ if (cfg.configVersion != 2 ||
+ cfg.generator != 'pub' ||
+ cfg.generatorVersion != sdk.version) {
+ badPackageConfig();
+ }
+
+ final packagePathsMapping = <String, String>{};
+ for (final pkg in cfg.packages) {
+ // Pub always sets packageUri = lib/
+ if (pkg.packageUri == null || pkg.packageUri.toString() != 'lib/') {
+ badPackageConfig();
+ }
+ packagePathsMapping[pkg.name] =
+ root.path('.dart_tool', p.fromUri(pkg.rootUri));
+ }
+ if (!_isPackagePathsMappingUptodateWithLockfile(packagePathsMapping)) {
+ outOfDate();
+ }
+
+ for (final pkg in cfg.packages) {
+ final id = lockFile.packages[pkg.name];
+ if (id == null) {
+ continue; // Ignore entries that aren't in the lockFile
}
- // Make sure that the packages file agrees with the lock file about the
- // path to the package.
- return p.normalize(packagesFilePath) == p.normalize(lockFilePath);
- });
+ final source = cache.source(id.source);
+ if (source is CachedSource) {
+ continue;
+ }
+
+ String languageVersion;
+ try {
+ languageVersion = extractLanguageVersion(
+ cache.load(id).pubspec.sdkConstraints[sdk.identifier],
+ );
+ } on FileException {
+ languageVersion = null;
+ }
+ if (languageVersion == null || pkg.languageVersion != languageVersion) {
+ dataError('${p.join(source.getDirectory(id), 'pubspec.yaml')} has '
+ 'changed since the pubspec.lock file was generated, please run "pub '
+ 'get" again.');
+ }
+ }
}
/// Saves a list of concrete package versions to the `pubspec.lock` file.
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index 796a4b2..e13c63e 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -205,7 +205,13 @@
var lockFile = result.lockFile;
_writeLockFile(dep.name, lockFile);
- writeTextFile(_getPackagesFilePath(dep.name), lockFile.packagesFile(cache));
+ // TODO(sigurdm): Use [Entrypoint.writePackagesFiles] instead.
+ final packagesFilePath = _getPackagesFilePath(dep.name);
+ final packageConfigFilePath = _getPackageConfigFilePath(dep.name);
+ writeTextFile(packagesFilePath, lockFile.packagesFile(cache));
+ ensureDir(p.dirname(packageConfigFilePath));
+ writeTextFile(
+ packageConfigFilePath, await lockFile.packageConfigFile(cache));
// Load the package graph from [result] so we don't need to re-parse all
// the pubspecs.
@@ -230,11 +236,21 @@
var binDir = p.join(_directory, packageName, 'bin');
cleanDir(binDir);
- var packagesFilePath = p.join(_directory, packageName, '.packages');
- if (!fileExists(packagesFilePath)) {
- // A .packages file may not already exist if the global executable has a
- // 1.6-style lock file instead.
- writeTextFile(packagesFilePath, entrypoint.packagesFileContents);
+ final packagesFilePath = _getPackagesFilePath(packageName);
+ final packageConfigFilePath = _getPackageConfigFilePath(packageName);
+ if (!fileExists(packagesFilePath) || !fileExists(packageConfigFilePath)) {
+ // TODO(sigurdm): Use [entrypoint.writePackagesFiles] instead.
+ // The `.packages` file may not already exist if the global executable
+ // has a 1.6-style lock file instead.
+ // Similarly, the `.dart_tool/package_config.json` may not exist if the
+ // global executable was activated before 2.6
+ writeTextFile(
+ packagesFilePath, entrypoint.lockFile.packagesFile(cache));
+ ensureDir(p.dirname(packageConfigFilePath));
+ writeTextFile(
+ packageConfigFilePath,
+ await entrypoint.lockFile.packageConfigFile(cache),
+ );
}
// Try to avoid starting up an asset server to precompile packages if
@@ -408,6 +424,11 @@
String _getPackagesFilePath(String name) =>
p.join(_directory, name, ".packages");
+ /// Gets the path to the `package_config.json` file for an
+ /// activated cached package with [name].
+ String _getPackageConfigFilePath(String name) =>
+ p.join(_directory, name, ".dart_tool", "package_config.json");
+
/// Shows the user a formatted list of globally activated packages.
void listActivePackages() {
if (!dirExists(_directory)) return;
diff --git a/lib/src/io.dart b/lib/src/io.dart
index 04008e4..bb4b839 100644
--- a/lib/src/io.dart
+++ b/lib/src/io.dart
@@ -161,7 +161,8 @@
/// Creates [file] and writes [contents] to it.
///
-/// If [dontLogContents] is true, the contents of the file will never be logged.
+/// If [dontLogContents] is `true`, the contents of the file will never be
+/// logged.
String writeTextFile(String file, String contents,
{bool dontLogContents = false, Encoding encoding}) {
encoding ??= utf8;
diff --git a/lib/src/lock_file.dart b/lib/src/lock_file.dart
index f9e32b6..6338d75 100644
--- a/lib/src/lock_file.dart
+++ b/lib/src/lock_file.dart
@@ -3,6 +3,7 @@
// BSD-style license that can be found in the LICENSE file.
import 'dart:collection';
+import 'dart:convert';
import 'package:collection/collection.dart';
import 'package:package_config/packages_file.dart' as packages_file;
@@ -12,7 +13,9 @@
import 'package:yaml/yaml.dart';
import 'io.dart';
+import 'package_config.dart';
import 'package_name.dart';
+import 'sdk.dart' show sdk;
import 'source_registry.dart';
import 'system_cache.dart';
import 'utils.dart';
@@ -230,6 +233,63 @@
return text.toString();
}
+ /// Returns the contents of the `.dart_tools/package_config` file generated
+ /// from this lockfile.
+ ///
+ /// This file is planned to eventually replace the `.packages` file.
+ ///
+ /// If [entrypoint] is passed, an accompanying [entrypointSdkConstraint]
+ /// should be given, these identifiy 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,
+ }) async {
+ final entries = <PackageConfigEntry>[];
+ for (final name in ordered(packages.keys)) {
+ final id = packages[name];
+ final source = cache.source(id.source);
+ final rootPath = source.getDirectory(id);
+ 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 source.describe(id);
+ final sdkConstraint = pubspec.sdkConstraints[sdk.identifier];
+ entries.add(PackageConfigEntry(
+ name: name,
+ rootUri: rootUri,
+ packageUri: p.toUri('lib/'),
+ languageVersion: extractLanguageVersion(sdkConstraint),
+ ));
+ }
+
+ if (entrypoint != null) {
+ entries.add(PackageConfigEntry(
+ name: entrypoint,
+ rootUri: p.toUri('../'),
+ packageUri: p.toUri('lib/'),
+ languageVersion: extractLanguageVersion(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
diff --git a/lib/src/package_config.dart b/lib/src/package_config.dart
new file mode 100644
index 0000000..b9f5caf
--- /dev/null
+++ b/lib/src/package_config.dart
@@ -0,0 +1,274 @@
+// Copyright (c) 2019, 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 'package:meta/meta.dart';
+
+import 'package:pub_semver/pub_semver.dart';
+import 'sdk.dart' show sdk;
+
+/// Contents of a `.dart_tool/package_config.json` file.
+class PackageConfig {
+ /// Version of the configuration in the `.dart_tool/package_config.json` file.
+ ///
+ /// The only supported value as of writing is `2`.
+ int configVersion;
+
+ /// Packages configured.
+ List<PackageConfigEntry> packages;
+
+ /// Date-time the `.dart_tool/package_config.json` file was generated.
+ ///
+ /// This property is **optional** and may be `null` if not given.
+ DateTime generated;
+
+ /// Tool that generated the `.dart_tool/package_config.json` file.
+ ///
+ /// For `pub` this is always `'pub'`.
+ ///
+ /// This property is **optional** and may be `null` if not given.
+ String generator;
+
+ /// Version of the tool that generated the `.dart_tool/package_config.json`
+ /// file.
+ ///
+ /// For `pub` this is the Dart SDK version from which `pub get` was called.
+ ///
+ /// This property is **optional** and may be `null` if not given.
+ Version generatorVersion;
+
+ /// Additional properties not in the specification for the
+ /// `.dart_tool/package_config.json` file.
+ Map<String, dynamic> additionalProperties;
+
+ PackageConfig({
+ @required this.configVersion,
+ @required this.packages,
+ this.generated,
+ this.generator,
+ this.generatorVersion,
+ this.additionalProperties,
+ }) {
+ additionalProperties ??= {};
+ }
+
+ /// Create [PackageConfig] from JSON [data].
+ ///
+ /// Throws [FormatException], if format is invalid, this does not validate the
+ /// contents only that the format is correct.
+ factory PackageConfig.fromJson(Object data) {
+ if (data is! Map<String, dynamic>) {
+ throw FormatException('package_config.json must be a JSON object');
+ }
+ final root = data as Map<String, dynamic>;
+
+ void _throw(String property, String mustBe) => throw FormatException(
+ '"$property" in .dart_tool/package_config.json $mustBe');
+
+ /// Read the 'configVersion' property
+ final configVersion = root['configVersion'];
+ if (configVersion is! int) {
+ _throw('configVersion', 'must be an integer');
+ }
+ if (configVersion != 2) {
+ _throw('configVersion', 'must be 2 (the only supported version)');
+ }
+
+ final packagesRaw = root['packages'];
+ if (packagesRaw is! List) {
+ _throw('packages', 'must be a list');
+ }
+ final packages = <PackageConfigEntry>[];
+ for (final entry in packagesRaw) {
+ packages.add(PackageConfigEntry.fromJson(entry));
+ }
+
+ // Read the 'generated' property
+ DateTime generated;
+ final generatedRaw = root['generated'];
+ if (generatedRaw != null) {
+ if (generatedRaw is! String) {
+ _throw('generated', 'must be a string, if given');
+ }
+ generated = DateTime.parse(generatedRaw);
+ }
+
+ // Read the 'generator' property
+ final generator = root['generator'];
+ if (generator != null && generator is! String) {
+ throw FormatException(
+ '"generator" in package_config.json must be a string, if given');
+ }
+
+ // Read the 'generatorVersion' property
+ Version generatorVersion;
+ final generatorVersionRaw = root['generatorVersion'];
+ if (generatorVersionRaw != null) {
+ if (generatorVersionRaw is! String) {
+ _throw('generatorVersion', 'must be a string, if given');
+ }
+ try {
+ generatorVersion = Version.parse(generatorVersionRaw);
+ } on FormatException catch (e) {
+ _throw('generatorVersion',
+ 'must be a semver version, if given, error: ${e.message}');
+ }
+ }
+
+ return PackageConfig(
+ configVersion: configVersion as int,
+ packages: packages,
+ generated: generated,
+ generator: generator,
+ generatorVersion: generatorVersion,
+ additionalProperties: Map.fromEntries(root.entries.where((e) => !{
+ 'configVersion',
+ 'packages',
+ 'generated',
+ 'generator',
+ 'generatorVersion',
+ }.contains(e.key))));
+ }
+
+ /// Convert to JSON structure.
+ Map<String, Object> toJson() => {
+ 'configVersion': configVersion,
+ 'packages': packages.map((p) => p.toJson()).toList(),
+ 'generated': generated?.toUtc()?.toIso8601String(),
+ 'generator': generator,
+ 'generatorVersion': generatorVersion?.toString(),
+ }..addAll(additionalProperties ?? {});
+}
+
+final _languageVersionPattern = RegExp(r'^\d+\.\d+$');
+
+class PackageConfigEntry {
+ /// Package name.
+ String name;
+
+ /// Root [Uri] of the package.
+ ///
+ /// This specifies the root folder of the package, all files below this folder
+ /// is considered part of this package.
+ Uri rootUri;
+
+ /// Relative URI path of the library folder relative to [rootUri].
+ ///
+ /// Import statements in Dart programs are resolved relative to this folder.
+ /// This must be in the sub-tree under [rootUri].
+ ///
+ /// This property is **optional** and may be `null` if not given.
+ Uri packageUri;
+
+ /// Language version used by package.
+ ///
+ /// Given as `<major>.<minor>` version, similar to the `// @dart = X.Y`
+ /// comment. This is derived from the lower-bound on the Dart SDK requirement
+ /// in the `pubspec.yaml` for the given package.
+ ///
+ /// This property is **optional** and may be `null` if not given.
+ String languageVersion;
+
+ /// Additional properties not in the specification for the
+ /// `.dart_tool/package_config.json` file.
+ Map<String, dynamic> additionalProperties;
+
+ PackageConfigEntry({
+ @required this.name,
+ @required this.rootUri,
+ this.packageUri,
+ this.languageVersion,
+ this.additionalProperties,
+ }) {
+ additionalProperties ??= {};
+ }
+
+ /// Create [PackageConfigEntry] from JSON [data].
+ ///
+ /// Throws [FormatException], if format is invalid, this does not validate the
+ /// contents only that the format is correct.
+ factory PackageConfigEntry.fromJson(Object data) {
+ if (data is! Map<String, dynamic>) {
+ throw FormatException(
+ 'packages[] entries in package_config.json must be JSON objects');
+ }
+ final root = data as Map<String, dynamic>;
+
+ void _throw(String property, String mustBe) => throw FormatException(
+ '"packages[].$property" in .dart_tool/package_config.json $mustBe');
+
+ final name = root['name'];
+ if (name is! String) {
+ _throw('name', 'must be a string');
+ }
+
+ Uri rootUri;
+ final rootUriRaw = root['rootUri'];
+ if (rootUriRaw is! String) {
+ _throw('rootUri', 'must be a string');
+ }
+ try {
+ rootUri = Uri.parse(rootUriRaw);
+ } on FormatException {
+ _throw('rootUri', 'must be a URI');
+ }
+
+ Uri packageUri;
+ final packageUriRaw = root['packageUri'];
+ if (packageUriRaw != null) {
+ if (packageUriRaw is! String) {
+ _throw('packageUri', 'must be a string');
+ }
+ try {
+ packageUri = Uri.parse(packageUriRaw);
+ } on FormatException {
+ _throw('packageUri', 'must be a URI');
+ }
+ }
+
+ final languageVersion = root['languageVersion'];
+ if (languageVersion != null) {
+ if (languageVersion is! String) {
+ _throw('languageVersion', 'must be a string');
+ }
+ if (!_languageVersionPattern.hasMatch(languageVersion)) {
+ _throw('languageVersion', 'must be on the form <major>.<minor>');
+ }
+ }
+
+ return PackageConfigEntry(
+ name: name,
+ rootUri: rootUri,
+ packageUri: packageUri,
+ languageVersion: languageVersion,
+ );
+ }
+
+ /// Convert to JSON structure.
+ Map<String, Object> toJson() => {
+ 'name': name,
+ 'rootUri': rootUri.toString(),
+ 'packageUri': packageUri?.toString(),
+ 'languageVersion': languageVersion,
+ }..addAll(additionalProperties ?? {});
+}
+
+/// Extract the _language version_ from an SDK constraint from `pubspec.yaml`.
+String extractLanguageVersion(VersionConstraint c) {
+ Version minVersion;
+ if (c == null || c.isEmpty) {
+ // If we have no language version, we use the version of the current
+ // SDK processing the 'pubspec.yaml' file.
+ minVersion = sdk.version;
+ } else if (c is Version) {
+ minVersion = c;
+ } else if (c is VersionRange) {
+ minVersion = c.min ?? sdk.version;
+ } else if (c is VersionUnion) {
+ // `ranges` is non-empty and sorted.
+ minVersion = c.ranges.first.min ?? sdk.version;
+ } else {
+ throw ArgumentError('Unknown VersionConstraint type $c.');
+ }
+ return '${minVersion.major}.${minVersion.minor}';
+}
diff --git a/lib/src/pubspec.dart b/lib/src/pubspec.dart
index 6307729..78e56df 100644
--- a/lib/src/pubspec.dart
+++ b/lib/src/pubspec.dart
@@ -448,7 +448,7 @@
/// 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 [PubspecError].
+ /// field, this will throw a [PubspecException].
factory Pubspec.load(String packageDir, SourceRegistry sources,
{String expectedName, bool includeDefaultSdkConstraint}) {
var pubspecPath = path.join(packageDir, 'pubspec.yaml');
diff --git a/pubspec.yaml b/pubspec.yaml
index b6700c3..d524cbe 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -2,7 +2,7 @@
publish_to: none
environment:
- sdk: ">=2.2.0 <3.0.0"
+ sdk: ">=2.3.0 <3.0.0"
dependencies:
# Note: Pub's test infrastructure assumes that any dependencies used in tests
diff --git a/test/descriptor.dart b/test/descriptor.dart
index f0d97c4..271fa2d 100644
--- a/test/descriptor.dart
+++ b/test/descriptor.dart
@@ -5,8 +5,11 @@
/// Pub-specific test descriptors.
import 'package:oauth2/oauth2.dart' as oauth2;
import 'package:pub/src/io.dart';
+import 'package:pub/src/package_config.dart';
import 'package:shelf_test_handler/shelf_test_handler.dart';
import 'package:test_descriptor/test_descriptor.dart';
+import 'package:meta/meta.dart';
+import 'package:path/path.dart' as p;
import 'descriptor/git.dart';
import 'descriptor/packages.dart';
@@ -173,6 +176,48 @@
Descriptor packagesFile([Map<String, String> dependencies]) =>
PackagesFileDescriptor(dependencies);
+/// Describes a `.dart_tools/package_config.json` file.
+///
+/// [dependencies] is a list of packages included in the file.
+///
+/// Validation checks that the `.dart_tools/package_config.json` file exists,
+/// has the expected entries (one per key in [dependencies]), each with a path
+/// that matches the `rootUri` of that package.
+Descriptor packageConfigFile(List<PackageConfigEntry> packages) =>
+ PackageConfigFileDescriptor(packages);
+
+/// Create a [PackageConfigEntry] which assumes package with [name] is either
+/// a cached package with given [version] or a path dependency at given [path].
+///
+/// If not given [languageVersion] will be inferred from current SDK version.
+PackageConfigEntry packageConfigEntry({
+ @required String name,
+ String version,
+ String path,
+ String languageVersion,
+}) {
+ if (version != null && path != null) {
+ throw ArgumentError.value(
+ path, 'path', 'Only one of "version" and "path" can be provided');
+ }
+ if (version == null && path == null) {
+ throw ArgumentError.value(
+ version, 'version', 'Either "version" or "path" must be given');
+ }
+ Uri rootUri;
+ if (version != null) {
+ rootUri = p.toUri(globalPackageServer.pathInCache(name, version));
+ } else {
+ rootUri = p.toUri(p.join('..', path));
+ }
+ return PackageConfigEntry(
+ name: name,
+ rootUri: rootUri,
+ packageUri: Uri(path: 'lib/'),
+ languageVersion: languageVersion ?? '0.1', // from '0.1.2+3'
+ );
+}
+
/// Describes a `.packages` file in the application directory, including the
/// implicit entry for the app itself.
Descriptor appPackagesFile(Map<String, String> dependencies) {
diff --git a/test/descriptor/packages.dart b/test/descriptor/packages.dart
index 8158313..b7b3385 100644
--- a/test/descriptor/packages.dart
+++ b/test/descriptor/packages.dart
@@ -3,21 +3,26 @@
// BSD-style license that can be found in the LICENSE file.
import "dart:async" show Future;
-import "dart:convert" show utf8;
+import "dart:convert" show JsonEncoder, json, utf8;
import "dart:io" show File;
import 'package:package_config/packages_file.dart' as packages_file;
import 'package:path/path.dart' as p;
+import 'package:pub/src/package_config.dart';
import 'package:pub_semver/pub_semver.dart';
import 'package:test/test.dart';
import 'package:test_descriptor/test_descriptor.dart';
import '../test_pub.dart';
+// Resolve against a dummy URL so that we can test whether the URLs in
+// the package file are themselves relative. We can't resolve against just
+// "." due to sdk#23809.
+final _base = "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p";
+
/// Describes a `.packages` file and its contents.
class PackagesFileDescriptor extends Descriptor {
- /// A map from package names to strings describing where the packages are
- /// located on disk.
+ /// A map from package names to either version strings or path to the package.
final Map<String, String> _dependencies;
/// Describes a `.packages` file with the given dependencies.
@@ -57,11 +62,7 @@
var bytes = await File(fullPath).readAsBytes();
- // Resolve against a dummy URL so that we can test whether the URLs in
- // the package file are themselves relative. We can't resolve against just
- // "." due to sdk#23809.
- var base = "/a/b/c/d/e/f/g/h/i/j/k/l/m/n/o/p";
- var map = packages_file.parse(bytes, Uri.parse(base));
+ var map = packages_file.parse(bytes, Uri.parse(_base));
for (var package in _dependencies.keys) {
if (!map.containsKey(package)) {
@@ -77,7 +78,7 @@
} else {
var expected = p.normalize(p.join(p.fromUri(description), 'lib'));
var actual = p.normalize(p.fromUri(
- p.url.relative(map[package].toString(), from: p.dirname(base))));
+ p.url.relative(map[package].toString(), from: p.dirname(_base))));
if (expected != actual) {
fail("Relative path: Expected $expected, found $actual");
@@ -94,17 +95,84 @@
}
}
- /// Returns `true` if [text] is a valid semantic version number string.
- bool _isSemver(String text) {
- try {
- // See if it's a semver.
- Version.parse(text);
- return true;
- } on FormatException catch (_) {
- // Do nothing.
+ String describe() => name;
+}
+
+/// Describes a `.dart_tools/package_config.json` file and its contents.
+class PackageConfigFileDescriptor extends Descriptor {
+ /// A map describing the packages in this `package_config.json` file.
+ final List<PackageConfigEntry> _packages;
+
+ PackageConfig get _config {
+ return PackageConfig(
+ configVersion: 2,
+ packages: _packages,
+ generatorVersion: Version.parse('0.1.2+3'),
+ generator: 'pub',
+ generated: DateTime.now().toUtc(),
+ );
+ }
+
+ /// Describes a `.packages` file with the given dependencies.
+ ///
+ /// [dependencies] maps package names to strings describing where the packages
+ /// are located on disk.
+ PackageConfigFileDescriptor(this._packages)
+ : super('.dart_tool/package_config.json');
+
+ Future<void> create([String parent]) async {
+ final packageConfigFile = File(p.join(parent ?? sandbox, name));
+ await packageConfigFile.parent.create();
+ await packageConfigFile.writeAsString(
+ const JsonEncoder.withIndent(' ').convert(_config.toJson()) + '\n',
+ );
+ }
+
+ Future<void> validate([String parent]) async {
+ final packageConfigFile = p.join(parent ?? sandbox, name);
+ if (!await File(packageConfigFile).exists()) {
+ fail("File not found: '$packageConfigFile'.");
}
- return false;
+
+ Map<String, Object> rawJson = json.decode(
+ await File(packageConfigFile).readAsString(),
+ );
+ PackageConfig config;
+ try {
+ config = PackageConfig.fromJson(rawJson);
+ } on FormatException catch (e) {
+ fail('File "$packageConfigFile" is not valid: $e');
+ }
+
+ // Compare packages as sets to ignore ordering.
+ expect(
+ config.packages.map((e) => e.toJson()).toSet(),
+ equals(_packages.map((e) => e.toJson()).toSet()),
+ reason:
+ '"packages" property in "$packageConfigFile" does not expected values',
+ );
+
+ final expected = PackageConfig.fromJson(_config.toJson());
+ // omit generated date-time and packages
+ expected.generated = null; // comparing timestamps is unnecessary.
+ config.generated = null;
+ expected.packages = []; // Already compared packages (ignoring ordering)
+ config.packages = [];
+ expect(config.toJson(), equals(expected.toJson()),
+ reason: '"$packageConfigFile" does not match expected values');
}
String describe() => name;
}
+
+/// Returns `true` if [text] is a valid semantic version number string.
+bool _isSemver(String text) {
+ try {
+ // See if it's a semver.
+ Version.parse(text);
+ return true;
+ } on FormatException catch (_) {
+ // Do nothing.
+ }
+ return false;
+}
diff --git a/test/get/package_name_test.dart b/test/get/package_name_test.dart
index 28326b4..febcd8a 100644
--- a/test/get/package_name_test.dart
+++ b/test/get/package_name_test.dart
@@ -57,7 +57,7 @@
await pubGet();
await d.dir(appPath, [
- d.packagesFile({"foo.bar.baz": "."})
+ d.packagesFile({"foo.bar.baz": "."}),
]).validate();
});
}
diff --git a/test/must_pub_get_test.dart b/test/must_pub_get_test.dart
index bf79dd5..1d0505b 100644
--- a/test/must_pub_get_test.dart
+++ b/test/must_pub_get_test.dart
@@ -40,7 +40,7 @@
'No pubspec.lock file found, please run "pub get" first.');
});
- group("there's no package spec", () {
+ group("there's no .packages", () {
setUp(() {
deleteEntry(p.join(d.sandbox, "myapp/.packages"));
});
@@ -48,6 +48,15 @@
_requiresPubGet('No .packages file found, please run "pub get" first.');
});
+ group("there's no package_config.json", () {
+ setUp(() {
+ deleteEntry(p.join(d.sandbox, "myapp/.dart_tool/package_config.json"));
+ });
+
+ _requiresPubGet(
+ 'No .dart_tool/package_config.json file found, please run "pub get" first.');
+ });
+
group("the pubspec has a new dependency", () {
setUp(() async {
await d.dir("foo", [d.libPubspec("foo", "1.0.0")]).create();
@@ -267,6 +276,40 @@
'file was generated, please run "pub get" again.');
});
+ group("the package_config.json file points to the wrong place", () {
+ setUp(() async {
+ await d.dir("bar", [d.libPubspec("foo", "1.0.0")]).create();
+
+ await d.dir(appPath, [
+ d.appPubspec({
+ "foo": {"path": "../bar"}
+ })
+ ]).create();
+
+ await pubGet();
+
+ await d.dir(appPath, [
+ d.packageConfigFile([
+ d.packageConfigEntry(
+ name: 'foo',
+ path: '../foo', // this is the wrong path
+ ),
+ d.packageConfigEntry(
+ name: 'myapp',
+ path: '.',
+ ),
+ ]),
+ ]).create();
+
+ // Ensure that the pubspec looks newer than the lockfile.
+ await _touch("pubspec.lock");
+ });
+
+ _requiresPubGet('The pubspec.lock file has changed since the '
+ '.dart_tool/package_config.json file was generated, '
+ 'please run "pub get" again.');
+ });
+
group("the lock file's SDK constraint doesn't match the current SDK", () {
setUp(() async {
// Avoid using a path dependency because it triggers the full validation
@@ -343,6 +386,45 @@
'since the pubspec.lock file was generated, please run "pub get" '
'again.');
});
+
+ group(
+ "a path dependency's language version doesn't match the package_config.json",
+ () {
+ setUp(() async {
+ await d.dir("bar", [
+ d.libPubspec(
+ "bar",
+ "1.0.0",
+ deps: {"foo": "1.0.0"},
+ // Creates language version requirement 0.0
+ sdk: '>= 0.0.1 <=0.9.9', // tests runs with '0.1.2+3'
+ ),
+ ]).create();
+
+ await d.dir(appPath, [
+ d.appPubspec({
+ "bar": {"path": "../bar"}
+ })
+ ]).create();
+
+ await pubGet();
+
+ // Update bar's pubspec without touching the app's.
+ await d.dir("bar", [
+ d.libPubspec(
+ "bar",
+ "1.0.0",
+ deps: {"foo": "1.0.0"},
+ // Creates language version requirement 0.1
+ sdk: '>= 0.1.0 <=0.9.9', // tests runs with '0.1.2+3'
+ ),
+ ]).create();
+ });
+
+ _requiresPubGet('${p.join('..', 'bar', 'pubspec.yaml')} has changed '
+ 'since the pubspec.lock file was generated, please run "pub get" '
+ 'again.');
+ });
});
group("doesn't require the user to run pub get first if", () {
@@ -353,9 +435,12 @@
await d.dir(appPath, [
d.appPubspec({"foo": "1.0.0"})
]).create();
+ // Ensure we get a new mtime (mtime is only reported with 1s precision)
+ await _touch('pubspec.yaml');
await _touch("pubspec.lock");
await _touch(".packages");
+ await _touch(".dart_tool/package_config.json");
});
_runsSuccessfully(runDeps: false);
@@ -396,7 +481,9 @@
_runsSuccessfully();
});
- group("the lockfile is newer than .packages, but they're up-to-date", () {
+ group(
+ "the lockfile is newer than .packages and package_config.json, but they're up-to-date",
+ () {
setUp(() async {
await d.dir(appPath, [
d.appPubspec({"foo": "1.0.0"})
@@ -498,9 +585,13 @@
File(p.join(d.sandbox, "myapp/pubspec.lock")).lastModifiedSync();
var packagesModified =
File(p.join(d.sandbox, "myapp/.packages")).lastModifiedSync();
+ var packageConfigModified =
+ File(p.join(d.sandbox, "myapp/.dart_tool/package_config.json"))
+ .lastModifiedSync();
expect(!pubspecModified.isAfter(lockFileModified), isTrue);
expect(!lockFileModified.isAfter(packagesModified), isTrue);
+ expect(!lockFileModified.isAfter(packageConfigModified), isTrue);
});
}
}
diff --git a/test/package_config_file_test.dart b/test/package_config_file_test.dart
new file mode 100644
index 0000000..6e935d4
--- /dev/null
+++ b/test/package_config_file_test.dart
@@ -0,0 +1,210 @@
+// Copyright (c) 2015, the Dart project authors. Please see the AUTHORS d.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 d.file.
+
+import 'package:test/test.dart';
+
+import 'package:pub/src/exit_codes.dart' as exit_codes;
+
+import 'descriptor.dart' as d;
+import 'test_pub.dart';
+
+main() {
+ forBothPubGetAndUpgrade((command) {
+ test('package_config.json file is created', () async {
+ await servePackages((builder) {
+ builder.serve("foo", "1.2.3",
+ deps: {'baz': '2.2.2'}, contents: [d.dir("lib", [])]);
+ builder.serve("bar", "3.2.1", contents: [d.dir("lib", [])]);
+ builder.serve("baz", "2.2.2",
+ deps: {"bar": "3.2.1"}, contents: [d.dir("lib", [])]);
+ });
+
+ await d.dir(appPath, [
+ d.appPubspec({"foo": "1.2.3"}),
+ d.dir('lib')
+ ]).create();
+
+ await pubCommand(command);
+
+ await d.dir(appPath, [
+ d.packageConfigFile([
+ d.packageConfigEntry(
+ name: 'foo',
+ version: '1.2.3',
+ ),
+ d.packageConfigEntry(
+ name: 'bar',
+ version: '3.2.1',
+ ),
+ d.packageConfigEntry(
+ name: 'baz',
+ version: '2.2.2',
+ ),
+ d.packageConfigEntry(
+ name: 'myapp',
+ path: '.',
+ ),
+ ]),
+ ]).validate();
+ });
+
+ test('package_config.json file is overwritten', () async {
+ await servePackages((builder) {
+ builder.serve("foo", "1.2.3",
+ deps: {'baz': '2.2.2'}, contents: [d.dir("lib", [])]);
+ builder.serve("bar", "3.2.1", contents: [d.dir("lib", [])]);
+ builder.serve("baz", "2.2.2",
+ deps: {"bar": "3.2.1"}, contents: [d.dir("lib", [])]);
+ });
+
+ await d.dir(appPath, [
+ d.appPubspec({"foo": "1.2.3"}),
+ d.dir('lib')
+ ]).create();
+
+ var oldFile = d.dir(appPath, [
+ d.packageConfigFile([
+ d.packageConfigEntry(
+ name: 'notFoo',
+ version: '9.9.9',
+ ),
+ ]),
+ ]);
+ await oldFile.create();
+ await oldFile.validate(); // Sanity-check that file was created correctly.
+
+ await pubCommand(command);
+
+ await d.dir(appPath, [
+ d.packageConfigFile([
+ d.packageConfigEntry(
+ name: 'foo',
+ version: '1.2.3',
+ ),
+ d.packageConfigEntry(
+ name: 'bar',
+ version: '3.2.1',
+ ),
+ d.packageConfigEntry(
+ name: 'baz',
+ version: '2.2.2',
+ ),
+ d.packageConfigEntry(
+ name: 'myapp',
+ path: '.',
+ ),
+ ]),
+ ]).validate();
+ });
+
+ test('package_config.json file is not created if pub fails', () async {
+ await d.dir(appPath, [
+ d.appPubspec({"foo": "1.2.3"}),
+ d.dir('lib')
+ ]).create();
+
+ await pubCommand(command,
+ args: ['--offline'], error: equalsIgnoringWhitespace("""
+ Because myapp depends on foo any which doesn't exist (could not find
+ package foo in cache), version solving failed.
+ """), exitCode: exit_codes.UNAVAILABLE);
+
+ await d.dir(appPath, [
+ d.nothing('.dart_tool/package_config.json'),
+ ]).validate();
+ });
+
+ test(
+ '.dart_tool/package_config.json file has relative path to path dependency',
+ () async {
+ await servePackages((builder) {
+ builder.serve("foo", "1.2.3",
+ deps: {'baz': 'any'}, contents: [d.dir("lib", [])]);
+ builder.serve("baz", "9.9.9", deps: {}, contents: [d.dir("lib", [])]);
+ });
+
+ await d.dir("local_baz", [
+ d.libDir("baz", 'baz 3.2.1'),
+ d.libPubspec("baz", "3.2.1")
+ ]).create();
+
+ await d.dir(appPath, [
+ d.pubspec({
+ "name": "myapp",
+ "dependencies": {
+ "foo": "^1.2.3",
+ },
+ "dependency_overrides": {
+ "baz": {"path": "../local_baz"},
+ }
+ }),
+ d.dir('lib')
+ ]).create();
+
+ await pubCommand(command);
+
+ await d.dir(appPath, [
+ d.packageConfigFile([
+ d.packageConfigEntry(
+ name: 'foo',
+ version: '1.2.3',
+ ),
+ d.packageConfigEntry(
+ name: 'baz',
+ path: '../local_baz',
+ ),
+ d.packageConfigEntry(
+ name: 'myapp',
+ path: '.',
+ ),
+ ]),
+ ]).validate();
+ });
+
+ test('package_config.json has language version', () async {
+ await servePackages((builder) {
+ builder.serve(
+ "foo",
+ "1.2.3",
+ pubspec: {
+ 'environment': {
+ 'sdk': '>=0.0.1 <=0.2.2+2', // tests runs with '0.1.2+3'
+ },
+ },
+ contents: [d.dir("lib", [])],
+ );
+ });
+
+ await d.dir(appPath, [
+ d.pubspec({
+ 'name': 'myapp',
+ 'dependencies': {
+ 'foo': '^1.2.3',
+ },
+ 'environment': {
+ 'sdk': '>=0.1.0 <=0.2.2+2', // tests runs with '0.1.2+3'
+ },
+ }),
+ d.dir('lib')
+ ]).create();
+
+ await pubCommand(command);
+
+ await d.dir(appPath, [
+ d.packageConfigFile([
+ d.packageConfigEntry(
+ name: 'foo',
+ version: '1.2.3',
+ languageVersion: '0.0',
+ ),
+ d.packageConfigEntry(
+ name: 'myapp',
+ path: '.',
+ languageVersion: '0.1',
+ ),
+ ]),
+ ]).validate();
+ });
+ });
+}
diff --git a/test/packages_file_test.dart b/test/packages_file_test.dart
index 9b84620..1585740 100644
--- a/test/packages_file_test.dart
+++ b/test/packages_file_test.dart
@@ -29,7 +29,7 @@
await d.dir(appPath, [
d.packagesFile(
- {"foo": "1.2.3", "bar": "3.2.1", "baz": "2.2.2", "myapp": "."})
+ {"foo": "1.2.3", "bar": "3.2.1", "baz": "2.2.2", "myapp": "."}),
]).validate();
});
@@ -91,7 +91,9 @@
await d.dir(appPath, [
d.pubspec({
"name": "myapp",
- "dependencies": {},
+ "dependencies": {
+ "foo": "^1.2.3",
+ },
"dependency_overrides": {
"baz": {"path": "../local_baz"},
}
@@ -102,7 +104,7 @@
await pubCommand(command);
await d.dir(appPath, [
- d.packagesFile({"myapp": ".", "baz": "../local_baz"})
+ d.packagesFile({"myapp": ".", "baz": "../local_baz", "foo": "1.2.3"}),
]).validate();
});
});