Index: lib/src/entrypoint.dart |
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart |
index 3c2fa25809eff4062f859eab320c927b99f9cacf..fd88b8da80cf42d7ec64f00d21f465121658f418 100644 |
--- a/lib/src/entrypoint.dart |
+++ b/lib/src/entrypoint.dart |
@@ -7,7 +7,8 @@ library pub.entrypoint; |
import 'dart:async'; |
import 'dart:io'; |
-import 'package:path/path.dart' as path; |
+import 'package:package_config/packages_file.dart' as packages_file; |
+import 'package:path/path.dart' as p; |
import 'package:barback/barback.dart'; |
import 'barback/asset_environment.dart'; |
@@ -201,13 +202,13 @@ class Entrypoint { |
// Just precompile the debug version of a package. We're mostly interested |
// in improving speed for development iteration loops, which usually use |
// debug mode. |
- var depsDir = path.join('.pub', 'deps', 'debug'); |
+ var depsDir = p.join('.pub', 'deps', 'debug'); |
var dependenciesToPrecompile = packageGraph.packages.values |
.where((package) { |
if (package.pubspec.transformers.isEmpty) return false; |
if (packageGraph.isPackageMutable(package.name)) return false; |
- if (!dirExists(path.join(depsDir, package.name))) return true; |
+ if (!dirExists(p.join(depsDir, package.name))) return true; |
if (changed == null) return true; |
/// Only recompile [package] if any of its transitive dependencies have |
@@ -222,12 +223,12 @@ class Entrypoint { |
if (dirExists(depsDir)) { |
// Delete any cached dependencies that are going to be recached. |
for (var package in dependenciesToPrecompile) { |
- deleteEntry(path.join(depsDir, package)); |
+ deleteEntry(p.join(depsDir, package)); |
} |
// Also delete any cached dependencies that should no longer be cached. |
for (var subdir in listDir(depsDir)) { |
- var package = packageGraph.packages[path.basename(subdir)]; |
+ var package = packageGraph.packages[p.basename(subdir)]; |
if (package == null || package.pubspec.transformers.isEmpty || |
packageGraph.isPackageMutable(package.name)) { |
deleteEntry(subdir); |
@@ -257,9 +258,9 @@ class Entrypoint { |
await waitAndPrintErrors(assets.map((asset) async { |
if (!dependenciesToPrecompile.contains(asset.id.package)) return; |
- var destPath = path.join( |
- depsDir, asset.id.package, path.fromUri(asset.id.path)); |
- ensureDir(path.dirname(destPath)); |
+ var destPath = p.join( |
+ depsDir, asset.id.package, p.fromUri(asset.id.path)); |
+ ensureDir(p.dirname(destPath)); |
await createFileFromStream(asset.read(), destPath); |
})); |
@@ -271,7 +272,7 @@ class Entrypoint { |
// assets (issue 19491), catch and handle compilation errors on a |
// per-package basis. |
for (var package in dependenciesToPrecompile) { |
- deleteEntry(path.join(depsDir, package)); |
+ deleteEntry(p.join(depsDir, package)); |
} |
rethrow; |
} |
@@ -282,8 +283,8 @@ class Entrypoint { |
Future precompileExecutables({Iterable<String> changed}) async { |
if (changed != null) changed = changed.toSet(); |
- var binDir = path.join('.pub', 'bin'); |
- var sdkVersionPath = path.join(binDir, 'sdk-version'); |
+ var binDir = p.join('.pub', 'bin'); |
+ var sdkVersionPath = p.join(binDir, 'sdk-version'); |
// If the existing executable was compiled with a different SDK, we need to |
// recompile regardless of what changed. |
@@ -297,7 +298,7 @@ class Entrypoint { |
for (var entry in listDir(binDir)) { |
if (!dirExists(entry)) continue; |
- var package = path.basename(entry); |
+ var package = p.basename(entry); |
if (!packageGraph.packages.containsKey(package) || |
packageGraph.isPackageMutable(package)) { |
deleteEntry(entry); |
@@ -337,7 +338,7 @@ class Entrypoint { |
}); |
await waitAndPrintErrors(executables.keys.map((package) async { |
- var dir = path.join(binDir, package); |
+ var dir = p.join(binDir, package); |
cleanDir(dir); |
await environment.precompileExecutables(package, dir, |
executableIds: executables[package]); |
@@ -373,8 +374,8 @@ class Entrypoint { |
// changed. Since we delete the bin directory before recompiling, we need to |
// recompile all executables. |
var executablesExist = executables.every((executable) => |
- fileExists(path.join('.pub', 'bin', packageName, |
- "${path.url.basename(executable.path)}.snapshot"))); |
+ fileExists(p.join('.pub', 'bin', packageName, |
+ "${p.url.basename(executable.path)}.snapshot"))); |
if (!executablesExist) return executables; |
// Otherwise, we don't need to recompile. |
@@ -396,7 +397,7 @@ class Entrypoint { |
return source.downloadToSystemCache(id); |
} |
- var packageDir = path.join(packagesDir, id.name); |
+ var packageDir = p.join(packagesDir, id.name); |
if (entryExists(packageDir)) deleteEntry(packageDir); |
return source.get(id, packageDir); |
}).then((_) => source.resolveId(id)); |
@@ -415,20 +416,127 @@ class Entrypoint { |
dataError('No .packages file found, please run "pub get" first.'); |
} |
- var packagesModified = new File(packagesFile).lastModifiedSync(); |
var pubspecModified = new File(pubspecPath).lastModifiedSync(); |
- if (packagesModified.isBefore(pubspecModified)) { |
- dataError('The pubspec.yaml file has changed since the .packages file ' |
- 'was generated, please run "pub get" again.'); |
+ var lockFileModified = new File(lockFilePath).lastModifiedSync(); |
+ |
+ var touchedLockFile = false; |
+ if (lockFileModified.isBefore(pubspecModified)) { |
+ if (_isLockFileUpToDate() && _arePackagesAvailable()) { |
+ touchedLockFile = true; |
+ touch(lockFilePath); |
+ } else { |
+ dataError('The pubspec.yaml file has changed since the pubspec.lock ' |
+ 'file was generated, please run "pub get" again.'); |
+ } |
} |
- var lockFileModified = new File(lockFilePath).lastModifiedSync(); |
+ var packagesModified = new File(packagesFile).lastModifiedSync(); |
if (packagesModified.isBefore(lockFileModified)) { |
- dataError('The pubspec.lock file has changed since the .packages file ' |
- 'was generated, please run "pub get" again.'); |
+ if (_isPackagesFileUpToDate()) { |
+ touch(packagesFile); |
+ } else { |
+ dataError('The pubspec.lock file has changed since the .packages file ' |
+ 'was generated, please run "pub get" again.'); |
+ } |
+ } else if (touchedLockFile) { |
+ touch(packagesFile); |
} |
} |
+ /// Determines whether or not the lockfile is out of date with respect to the |
+ /// pubspec. |
+ /// |
+ /// This will be `false` if the pubspec contains dependencies that are not in |
+ /// the lockfile or that don't match what's in there. |
+ bool _isLockFileUpToDate() { |
+ return root.immediateDependencies.every((package) { |
+ var locked = lockFile.packages[package.name]; |
+ if (locked == null) return false; |
+ |
+ if (package.source != locked.source) return false; |
+ |
+ if (!package.constraint.allows(locked.version)) return false; |
+ |
+ var source = cache.sources[package.source]; |
+ if (source == null) return false; |
+ |
+ return source.descriptionsEqual(package.description, locked.description); |
+ }); |
+ } |
+ |
+ /// Determines whether all of the packages in the lockfile are already |
+ /// installed and available. |
+ /// |
+ /// Note: this assumes [_isLockFileUpToDate] has already been called and |
+ /// returned `true`. |
+ bool _arePackagesAvailable() { |
+ return lockFile.packages.values.every((package) { |
+ var source = cache.sources[package.source]; |
+ |
+ // This should only be called after [_isLockFileUpToDate] has returned |
+ // `true`, which ensures all of the sources in the lock file are valid. |
+ assert(source != null); |
+ |
+ // We only care about cached sources. Uncached sources aren't "installed". |
+ // If one of those is missing, we want to show the user the file not |
+ // found error later since installing won't accomplish anything. |
+ if (source is! CachedSource) return true; |
+ |
+ // Get the directory. |
+ var dir = source.getDirectory(package); |
+ // See if the directory is there and looks like a package. |
+ return dirExists(dir) && fileExists(p.join(dir, "pubspec.yaml")); |
+ }); |
+ } |
+ |
+ /// Determines whether or not the `.packages` file is out of date with respect |
+ /// to the lockfile. |
+ /// |
+ /// 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() { |
+ var packages = packages_file.parse( |
+ new File(packagesFile).readAsBytesSync(), |
+ p.toUri(packagesFile)); |
+ |
+ return lockFile.packages.values.every((lockFileId) { |
+ var source = cache.sources[lockFileId.source]; |
+ |
+ // 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 (source == null) return false; |
+ |
+ var packagesFileUri = packages[lockFileId.name]; |
+ if (packagesFileUri == null) return false; |
+ |
+ // Pub only generates "file:" and relative URIs. |
+ if (packagesFileUri.scheme != 'file' && |
+ packagesFileUri.scheme.isNotEmpty) { |
+ return false; |
+ } |
+ |
+ // 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)); |
+ |
+ // 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; |
+ } |
+ |
+ // Make sure that the packages file agrees with the lock file about the |
+ // path to the package. |
+ return p.normalize(packagesFilePath) == p.normalize(lockFilePath); |
+ }); |
+ } |
+ |
/// Saves a list of concrete package versions to the `pubspec.lock` file. |
void _saveLockFile(List<PackageId> packageIds) { |
_lockFile = new LockFile(packageIds, cache.sources); |
@@ -439,7 +547,7 @@ class Entrypoint { |
/// Creates a self-referential symlink in the `packages` directory that allows |
/// a package to import its own files using `package:`. |
void _linkSelf() { |
- var linkPath = path.join(packagesDir, root.name); |
+ var linkPath = p.join(packagesDir, root.name); |
// Create the symlink if it doesn't exist. |
if (entryExists(linkPath)) return; |
ensureDir(packagesDir); |
@@ -482,7 +590,7 @@ class Entrypoint { |
/// files and `package` files. |
List<String> _listDirWithoutPackages(dir) { |
return flatten(listDir(dir).map((file) { |
- if (path.basename(file) == 'packages') return []; |
+ if (p.basename(file) == 'packages') return []; |
if (!dirExists(file)) return []; |
var fileAndSubfiles = [file]; |
fileAndSubfiles.addAll(_listDirWithoutPackages(file)); |
@@ -495,7 +603,7 @@ class Entrypoint { |
/// |
/// Otherwise, deletes a "packages" directories in [dir] if one exists. |
void _linkOrDeleteSecondaryPackageDir(String dir) { |
- var symlink = path.join(dir, 'packages'); |
+ var symlink = p.join(dir, 'packages'); |
if (entryExists(symlink)) deleteEntry(symlink); |
if (_packageSymlinks) createSymlink(packagesDir, symlink, relative: true); |
} |