dart pub get --enforce-lockfile (#3637)
diff --git a/lib/src/command/get.dart b/lib/src/command/get.dart
index 251028e..96cf525 100644
--- a/lib/src/command/get.dart
+++ b/lib/src/command/get.dart
@@ -28,6 +28,13 @@
negatable: false,
help: "Report what dependencies would change but don't change any.");
+ argParser.addFlag(
+ 'enforce-lockfile',
+ negatable: false,
+ help:
+ 'Enforce pubspec.lock. Fail resolution if pubspec.lock does not satisfy pubspec.yaml',
+ );
+
argParser.addFlag('precompile',
help: 'Build executables in immediate dependencies.');
@@ -55,6 +62,7 @@
dryRun: argResults['dry-run'],
precompile: argResults['precompile'],
analytics: analytics,
+ enforceLockfile: argResults['enforce-lockfile'],
);
var example = entrypoint.example;
@@ -65,6 +73,7 @@
precompile: argResults['precompile'],
analytics: analytics,
onlyReportSuccessOrFailure: true,
+ enforceLockfile: argResults['enforce-lockfile'],
);
}
}
diff --git a/lib/src/entrypoint.dart b/lib/src/entrypoint.dart
index ab5b80c..136b325 100644
--- a/lib/src/entrypoint.dart
+++ b/lib/src/entrypoint.dart
@@ -309,6 +309,10 @@
/// shown --- in case of failure, a reproduction command is shown.
///
/// Updates [lockFile] and [packageRoot] accordingly.
+ ///
+ /// If [enforceLockfile] is true no changes to the current lockfile are
+ /// allowed. Instead the existing lockfile is loaded, verified against
+ /// pubspec.yaml and all dependencies downloaded.
Future<void> acquireDependencies(
SolveType type, {
Iterable<String>? unlock,
@@ -316,13 +320,32 @@
bool precompile = false,
required PubAnalytics? analytics,
bool onlyReportSuccessOrFailure = false,
+ bool enforceLockfile = false,
}) async {
+ final suffix = root.isInMemory || root.dir == '.' ? '' : ' in ${root.dir}';
+
+ String forDetails() {
+ if (!onlyReportSuccessOrFailure) return '';
+ final enforceLockfileOption =
+ enforceLockfile ? ' --enforce-lockfile' : '';
+ final directoryOption =
+ root.isInMemory || root.dir == '.' ? '' : ' --directory ${root.dir}';
+ return ' For details run `$topLevelProgram pub ${type.toString()}$directoryOption$enforceLockfileOption`';
+ }
+
+ if (enforceLockfile && !fileExists(lockFilePath)) {
+ throw ApplicationException('''
+Retrieving dependencies failed$suffix.
+Cannot do `--enforce-lockfile` without an existing `pubspec.lock`.
+
+Try running `$topLevelProgram pub get` to create `$lockFilePath`.''');
+ }
+
if (!onlyReportSuccessOrFailure && hasPubspecOverrides) {
log.warning(
'Warning: pubspec.yaml has overrides from $pubspecOverridesPath');
}
- final suffix = root.isInMemory || root.dir == '.' ? '' : ' in ${root.dir}';
SolveResult result;
try {
result = await log.progress('Resolving dependencies$suffix', () async {
@@ -337,11 +360,8 @@
});
} catch (e) {
if (onlyReportSuccessOrFailure && (e is ApplicationException)) {
- final directoryOption = root.isInMemory || root.dir == '.'
- ? ''
- : ' --directory ${root.dir}';
throw ApplicationException(
- 'Resolving dependencies$suffix failed. For details run `$topLevelProgram pub ${type.toString()}$directoryOption`');
+ 'Resolving dependencies$suffix failed.${forDetails()}');
} else {
rethrow;
}
@@ -352,22 +372,34 @@
final newLockFile = await result.downloadCachedPackages(cache);
final report = SolveReport(
- type, root, lockFile, newLockFile, result.availableVersions, cache,
- dryRun: dryRun);
- if (!onlyReportSuccessOrFailure) {
- await report.show();
- }
- _lockFile = newLockFile;
+ type,
+ root,
+ lockFile,
+ newLockFile,
+ result.availableVersions,
+ cache,
+ dryRun: dryRun,
+ enforceLockfile: enforceLockfile,
+ quiet: onlyReportSuccessOrFailure,
+ );
- if (!dryRun) {
+ final hasChanges = await report.show();
+ await report.summarize();
+ if (enforceLockfile && hasChanges) {
+ var suggestion = onlyReportSuccessOrFailure
+ ? ''
+ : '''
+\n\nTo update `$lockFilePath` run `$topLevelProgram pub get`$suffix without
+`--enforce-lockfile`.''';
+ dataError('''
+Unable to satisfy `$pubspecPath` using `$lockFilePath`$suffix.${forDetails()}$suggestion''');
+ }
+
+ if (!(dryRun || enforceLockfile)) {
newLockFile.writeToFile(lockFilePath, cache);
}
- if (onlyReportSuccessOrFailure) {
- log.message('Got dependencies$suffix.');
- } else {
- await report.summarize();
- }
+ _lockFile = newLockFile;
if (!dryRun) {
if (analytics != null) {
diff --git a/lib/src/global_packages.dart b/lib/src/global_packages.dart
index 0cbb15a..a39536f 100644
--- a/lib/src/global_packages.dart
+++ b/lib/src/global_packages.dart
@@ -245,6 +245,8 @@
result.availableVersions,
cache,
dryRun: false,
+ quiet: false,
+ enforceLockfile: false,
).show();
}
diff --git a/lib/src/solver/report.dart b/lib/src/solver/report.dart
index 176ab67..e9cd00e 100644
--- a/lib/src/solver/report.dart
+++ b/lib/src/solver/report.dart
@@ -2,7 +2,6 @@
// 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:path/path.dart' as path;
import 'package:pub_semver/pub_semver.dart';
import '../command_runner.dart';
@@ -30,6 +29,11 @@
final SystemCache _cache;
final bool _dryRun;
+ /// If quiet only a single summary line is output.
+ final bool _quiet;
+
+ final bool _enforceLockfile;
+
/// The available versions of all selected packages from their source.
///
/// An entry here may not include the full list of versions available if the
@@ -38,8 +42,6 @@
/// Version list will not contain any retracted package versions.
final Map<String, List<Version>> _availableVersions;
- final _output = StringBuffer();
-
SolveReport(
this._type,
this._root,
@@ -48,14 +50,22 @@
this._availableVersions,
this._cache, {
required bool dryRun,
- }) : _dryRun = dryRun;
+ required bool enforceLockfile,
+ required bool quiet,
+ }) : _dryRun = dryRun,
+ _quiet = quiet,
+ _enforceLockfile = enforceLockfile;
/// Displays a report of the results of the version resolution in
/// [_newLockFile] relative to the [_previousLockFile] file.
- Future<void> show() async {
- await _reportChanges();
- await _reportOverrides();
+ ///
+ /// Returns `true` if there was any change of dependencies relative to the old
+ /// lockfile.
+ Future<bool> show() async {
+ final hasChanges = await _reportChanges();
_checkContentHashesMatchOldLockfile();
+ await _reportOverrides();
+ return hasChanges;
}
void _checkContentHashesMatchOldLockfile() {
@@ -100,14 +110,14 @@
}
if (issues.isNotEmpty) {
- log.warning('''
+ warning('''
The existing content-hash from pubspec.lock doesn't match contents for:
* ${issues.join('\n * ')}
This indicates one of:
* The content has changed on the server since you created the pubspec.lock.
* The pubspec.lock has been corrupted.
-${_dryRun ? '' : '\nThe content-hashes in pubspec.lock has been updated.'}
+${_dryRun || _enforceLockfile ? '' : '\nThe content-hashes in pubspec.lock has been updated.'}
For more information see:
$contentHashesDocumentationUrl
@@ -118,7 +128,8 @@
/// Displays a one-line message summarizing what changes were made (or would
/// be made) to the lockfile.
///
- /// If [dryRun] is true, describes it in terms of what would be done.
+ /// If [_dryRun] or [_enforceLockfile] is true, describes it in terms of what
+ /// would be done.
///
/// [type] is the type of version resolution that was run.
@@ -145,77 +156,102 @@
var suffix = '';
if (!_root.isInMemory) {
- final dir = path.normalize(_root.dir);
+ final dir = _root.dir;
if (dir != '.') {
suffix = ' in $dir';
}
}
- if (_dryRun) {
- if (numChanged == 0) {
- log.message('No dependencies would change$suffix.');
- } else if (numChanged == 1) {
- log.message('Would change $numChanged dependency$suffix.');
+ if (_quiet) {
+ if (_dryRun) {
+ log.message('Would get dependencies$suffix.');
+ } else if (_enforceLockfile) {
+ if (numChanged == 0) {
+ log.message('Got dependencies$suffix.');
+ }
} else {
- log.message('Would change $numChanged dependencies$suffix.');
+ log.message('Got dependencies$suffix.');
}
} else {
- if (numChanged == 0) {
- if (_type == SolveType.get) {
- log.message('Got dependencies$suffix!');
+ if (_dryRun) {
+ if (numChanged == 0) {
+ log.message('No dependencies would change$suffix.');
+ } else if (numChanged == 1) {
+ log.message('Would change $numChanged dependency$suffix.');
} else {
- log.message('No dependencies changed$suffix.');
+ log.message('Would change $numChanged dependencies$suffix.');
}
- } else if (numChanged == 1) {
- log.message('Changed $numChanged dependency$suffix!');
+ } else if (_enforceLockfile) {
+ if (numChanged == 0) {
+ log.message('Got dependencies$suffix!');
+ } else if (numChanged == 1) {
+ log.message('Would change $numChanged dependency$suffix.');
+ } else {
+ log.message('Would change $numChanged dependencies$suffix.');
+ }
} else {
- log.message('Changed $numChanged dependencies$suffix!');
+ if (numChanged == 0) {
+ if (_type == SolveType.get) {
+ log.message('Got dependencies$suffix!');
+ } else {
+ log.message('No dependencies changed$suffix.');
+ }
+ } else if (numChanged == 1) {
+ log.message('Changed $numChanged dependency$suffix!');
+ } else {
+ log.message('Changed $numChanged dependencies$suffix!');
+ }
}
- }
- if (_type == SolveType.upgrade) {
- await reportDiscontinued();
- reportOutdated();
+ if (_type == SolveType.upgrade) {
+ await reportDiscontinued();
+ reportOutdated();
+ }
}
}
/// Displays a report of all of the previous and current dependencies and
/// how they have changed.
- Future<void> _reportChanges() async {
- _output.clear();
-
+ ///
+ /// Returns true if anything changed.
+ Future<bool> _reportChanges() async {
+ final output = StringBuffer();
// Show the new set of dependencies ordered by name.
var names = _newLockFile.packages.keys.toList();
names.remove(_root.name);
names.sort();
+ var hasChanges = false;
for (final name in names) {
- await _reportPackage(name);
+ hasChanges |= await _reportPackage(name, output);
}
// Show any removed ones.
var removed = _previousLockFile.packages.keys.toSet();
removed.removeAll(names);
removed.remove(_root.name); // Never consider root.
if (removed.isNotEmpty) {
- _output.writeln('These packages are no longer being depended on:');
+ output.writeln('These packages are no longer being depended on:');
for (var name in ordered(removed)) {
- await _reportPackage(name, alwaysShow: true);
+ await _reportPackage(name, output, alwaysShow: true);
}
+ hasChanges = true;
}
- log.message(_output);
+ message(output.toString());
+ return hasChanges;
}
/// Displays a warning about the overrides currently in effect.
Future<void> _reportOverrides() async {
- _output.clear();
+ final output = StringBuffer();
if (_root.dependencyOverrides.isNotEmpty) {
- _output.writeln('Warning: You are using these overridden dependencies:');
+ output.writeln('Warning: You are using these overridden dependencies:');
for (var name in ordered(_root.dependencyOverrides.keys)) {
- await _reportPackage(name, alwaysShow: true, highlightOverride: false);
+ await _reportPackage(name, output,
+ alwaysShow: true, highlightOverride: false);
}
- log.warning(_output);
+ warning(output.toString());
}
}
@@ -235,9 +271,9 @@
}
if (numDiscontinued > 0) {
if (numDiscontinued == 1) {
- log.message('1 package is discontinued.');
+ message('1 package is discontinued.');
} else {
- log.message('$numDiscontinued packages are discontinued.');
+ message('$numDiscontinued packages are discontinued.');
}
}
}
@@ -263,7 +299,7 @@
} else {
packageCountString = '$outdatedPackagesCount packages have';
}
- log.message('$packageCountString newer versions incompatible with '
+ message('$packageCountString newer versions incompatible with '
'dependency constraints.\nTry `$topLevelProgram pub outdated` for more information.');
}
}
@@ -273,7 +309,9 @@
/// If [alwaysShow] is true, the package is reported even if it didn't change,
/// regardless of [_type]. If [highlightOverride] is true (or absent), writes
/// "(override)" next to overridden packages.
- Future<void> _reportPackage(String name,
+ ///
+ /// Returns true if the package had changed.
+ Future<bool> _reportPackage(String name, StringBuffer output,
{bool alwaysShow = false, bool highlightOverride = true}) async {
var newId = _newLockFile.packages[name];
var oldId = _previousLockFile.packages[name];
@@ -380,38 +418,55 @@
if (_type == SolveType.get &&
!(alwaysShow || changed || addedOrRemoved || message != null)) {
- return;
+ return changed || addedOrRemoved;
}
- _output.write(icon);
- _output.write(log.bold(id.name));
- _output.write(' ');
- _writeId(id);
+ output.write(icon);
+ output.write(log.bold(id.name));
+ output.write(' ');
+ _writeId(id, output);
// If the package was upgraded, show what it was upgraded from.
if (changed) {
- _output.write(' (was ');
- _writeId(oldId!);
- _output.write(')');
+ output.write(' (was ');
+ _writeId(oldId!, output);
+ output.write(')');
}
// Highlight overridden packages.
if (isOverridden && highlightOverride) {
- _output.write(" ${log.magenta('(overridden)')}");
+ output.write(" ${log.magenta('(overridden)')}");
}
- if (message != null) _output.write(' ${log.cyan(message)}');
+ if (message != null) output.write(' ${log.cyan(message)}');
- _output.writeln();
+ output.writeln();
+ return changed || addedOrRemoved;
}
/// Writes a terse description of [id] (not including its name) to the output.
- void _writeId(PackageId id) {
- _output.write(id.version);
+ void _writeId(PackageId id, StringBuffer output) {
+ output.write(id.version);
if (id.source != _cache.defaultSource) {
var description = id.description.format();
- _output.write(' from ${id.source} $description');
+ output.write(' from ${id.source} $description');
+ }
+ }
+
+ void warning(String message) {
+ if (_quiet) {
+ log.fine(message);
+ } else {
+ log.warning(message);
+ }
+ }
+
+ void message(String message) {
+ if (_quiet) {
+ log.fine(message);
+ } else {
+ log.message(message);
}
}
}
diff --git a/test/get/enforce_lockfile_test.dart b/test/get/enforce_lockfile_test.dart
new file mode 100644
index 0000000..979db08
--- /dev/null
+++ b/test/get/enforce_lockfile_test.dart
@@ -0,0 +1,188 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:io';
+
+import 'package:path/path.dart' as p;
+import 'package:pub/src/exit_codes.dart';
+import 'package:test/test.dart';
+
+import '../descriptor.dart';
+import '../test_pub.dart';
+
+Future<void> main() async {
+ test('Recreates .dart_tool/package_config.json, redownloads archives',
+ () async {
+ final server = await servePackages();
+ server.serve('foo', '1.0.0');
+ await appDir({'foo': 'any'}).create();
+ await pubGet();
+ final packageConfig =
+ File(path(p.join(appPath, '.dart_tool', 'package_config.json')));
+ packageConfig.deleteSync();
+ await runPub(args: ['cache', 'clean', '-f']);
+ await pubGet(args: ['--enforce-lockfile']);
+ expect(packageConfig.existsSync(), isTrue);
+ await cacheDir({'foo': '1.0.0'}).validate();
+ await appPackageConfigFile([
+ packageConfigEntry(name: 'foo', version: '1.0.0'),
+ ]).validate();
+ });
+
+ test('Refuses to get if no lockfile exists', () async {
+ await appDir({}).create();
+ await pubGet(
+ args: ['--enforce-lockfile'],
+ error: '''
+Retrieving dependencies failed.
+Cannot do `--enforce-lockfile` without an existing `pubspec.lock`.
+
+Try running `dart pub get` to create `pubspec.lock`.
+''',
+ );
+ });
+
+ test('Refuses to get in ./example if hash is updated', () async {
+ final server = await servePackages();
+ server.serveContentHashes = true;
+ server.serve('foo', '1.0.0');
+ server.serve('bar', '1.0.0');
+
+ await appDir({'foo': '^1.0.0'}).create();
+ await dir(appPath, [
+ dir('example', [
+ libPubspec('example', '0.0.0', deps: {
+ 'bar': '1.0.0',
+ 'myapp': {'path': '../'}
+ })
+ ])
+ ]).create();
+ await pubGet(args: ['--example']);
+
+ server.serve('bar', '1.0.0', contents: [
+ file('README.md', 'Including this will change the content-hash.'),
+ ]);
+ // Deleting the version-listing cache will cause it to be refetched, and the
+ // error will happen.
+ File(p.join(globalServer.cachingPath, '.cache', 'bar-versions.json'))
+ .deleteSync();
+
+ final example = p.join('.', 'example');
+ final examplePubspec = p.join('example', 'pubspec.yaml');
+ final exampleLockfile = p.join('example', 'pubspec.lock');
+
+ await pubGet(
+ args: ['--enforce-lockfile', '--example'],
+ output: allOf(
+ contains('Got dependencies!'),
+ contains('Resolving dependencies in $example...'),
+ ),
+ error: contains(
+ 'Unable to satisfy `$examplePubspec` using `$exampleLockfile` in $example. For details run `dart pub get --directory $example --enforce-lockfile'),
+ exitCode: DATA,
+ );
+ });
+
+ test('Refuses to get if lockfile is missing package', () async {
+ final server = await servePackages();
+ server.serve('foo', '1.0.0');
+ await appDir({}).create();
+ await pubGet();
+ await appDir({'foo': 'any'}).create();
+
+ await pubGet(
+ args: ['--enforce-lockfile'],
+ output: allOf(
+ contains('+ foo 1.0.0'),
+ contains('Would have changed 1 dependency.'),
+ ),
+ error: contains('Unable to satisfy `pubspec.yaml` using `pubspec.lock`.'),
+ exitCode: DATA,
+ );
+ });
+
+ test('Refuses to get if package is locked to version not matching constraint',
+ () async {
+ final server = await servePackages();
+ server.serve('foo', '1.0.0');
+ server.serve('foo', '2.0.0');
+ await appDir({'foo': '^1.0.0'}).create();
+ await pubGet();
+ await appDir({'foo': '^2.0.0'}).create();
+ await pubGet(
+ args: ['--enforce-lockfile'],
+ output: allOf([
+ contains('> foo 2.0.0 (was 1.0.0)'),
+ contains('Would have changed 1 dependency.'),
+ ]),
+ error: contains('Unable to satisfy `pubspec.yaml` using `pubspec.lock`.'),
+ exitCode: DATA,
+ );
+ });
+
+ test("Refuses to get if hash on server doesn't correspond to lockfile",
+ () async {
+ final server = await servePackages();
+ server.serveContentHashes = true;
+ server.serve('foo', '1.0.0');
+ await appDir({'foo': '^1.0.0'}).create();
+ await pubGet();
+ server.serve('foo', '1.0.0', contents: [
+ file('README.md', 'Including this will change the content-hash.'),
+ ]);
+ // Deleting the version-listing cache will cause it to be refetched, and the
+ // error will happen.
+ File(p.join(globalServer.cachingPath, '.cache', 'foo-versions.json'))
+ .deleteSync();
+ await pubGet(
+ args: ['--enforce-lockfile'],
+ output: allOf(
+ contains('~ foo 1.0.0 (was 1.0.0)'),
+ contains('Would have changed 1 dependency.'),
+ ),
+ error: allOf(
+ contains('Cached version of foo-1.0.0 has wrong hash - redownloading.'),
+ contains(
+ 'The existing content-hash from pubspec.lock doesn\'t match contents for:',
+ ),
+ contains(
+ ' * foo-1.0.0 from "${server.url}"',
+ ),
+ contains(
+ 'Unable to satisfy `pubspec.yaml` using `pubspec.lock`.',
+ ),
+ ),
+ exitCode: DATA,
+ );
+ });
+
+ test(
+ 'Refuses to get if archive on legacy server doesn\'t have hash corresponding to lockfile',
+ () async {
+ final server = await servePackages();
+ server.serveContentHashes = false;
+ server.serve('foo', '1.0.0');
+ await appDir({'foo': '^1.0.0'}).create();
+ await pubGet();
+ await runPub(args: ['cache', 'clean', '-f']);
+ server.serve('foo', '1.0.0', contents: [
+ file('README.md', 'Including this will change the content-hash.'),
+ ]);
+
+ await pubGet(
+ args: ['--enforce-lockfile'],
+ output: allOf(
+ contains('~ foo 1.0.0 (was 1.0.0)'),
+ contains('Would have changed 1 dependency.'),
+ ),
+ error: allOf(
+ contains('''
+The existing content-hash from pubspec.lock doesn't match contents for:
+ * foo-1.0.0 from "${server.url}"'''),
+ contains('Unable to satisfy `pubspec.yaml` using `pubspec.lock`.'),
+ ),
+ exitCode: DATA,
+ );
+ });
+}
diff --git a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
index f48d6ba..642f8fd 100644
--- a/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
+++ b/test/testdata/goldens/embedding/embedding_test/logfile is written with --verbose and on unexpected exceptions.txt
@@ -247,6 +247,7 @@
| For details on how manage the `PUB_CACHE`, see:
| https://dart.dev/go/pub-cache
MSG : + foo 1.0.0
+MSG : Changed 1 dependency!
IO : Writing $N characters to text file pubspec.lock.
FINE: Contents:
| # Generated by pub
@@ -262,7 +263,6 @@
| version: "1.0.0"
| sdks:
| dart: ">=0.1.2 <1.0.0"
-MSG : Changed 1 dependency!
IO : Writing $N characters to text file .dart_tool/package_config.json.
FINE: Contents:
| {
diff --git a/test/testdata/goldens/help_test/pub get --help.txt b/test/testdata/goldens/help_test/pub get --help.txt
index 74648a2..45d987b 100644
--- a/test/testdata/goldens/help_test/pub get --help.txt
+++ b/test/testdata/goldens/help_test/pub get --help.txt
@@ -5,12 +5,14 @@
Get the current package's dependencies.
Usage: pub get <subcommand> [arguments...]
--h, --help Print this usage information.
- --[no-]offline Use cached packages instead of accessing the network.
--n, --dry-run Report what dependencies would change but don't change
- any.
- --[no-]precompile Build executables in immediate dependencies.
--C, --directory=<dir> Run this in the directory<dir>.
+-h, --help Print this usage information.
+ --[no-]offline Use cached packages instead of accessing the network.
+-n, --dry-run Report what dependencies would change but don't change
+ any.
+ --enforce-lockfile Enforce pubspec.lock. Fail resolution if pubspec.lock
+ does not satisfy pubspec.yaml
+ --[no-]precompile Build executables in immediate dependencies.
+-C, --directory=<dir> Run this in the directory<dir>.
Run "pub help" to see global options.
See https://dart.dev/tools/pub/cmd/pub-get for detailed documentation.