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();
     });
   });