1// Copyright 2014 The Flutter Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5/// @docImport '../localizations/gen_l10n.dart';
6library;
7
8import 'dart:async';
9
10import 'package:meta/meta.dart';
11import 'package:process/process.dart';
12import '../base/bot_detector.dart';
13import '../base/common.dart';
14import '../base/context.dart';
15import '../base/file_system.dart';
16import '../base/io.dart' as io;
17import '../base/io.dart';
18import '../base/logger.dart';
19import '../base/platform.dart';
20import '../base/process.dart';
21import '../cache.dart';
22import '../convert.dart';
23import '../dart/package_map.dart';
24import '../project.dart';
25import '../version.dart';
26
27/// The [Pub] instance.
28Pub get pub => context.get<Pub>()!;
29
30/// The console environment key used by the pub tool.
31const _kPubEnvironmentKey = 'PUB_ENVIRONMENT';
32
33/// The console environment key used by the pub tool to find the cache directory.
34const _kPubCacheEnvironmentKey = 'PUB_CACHE';
35
36typedef MessageFilter = String? Function(String message);
37
38bool _tryDeleteDirectory(Directory directory, Logger logger) {
39 try {
40 if (directory.existsSync()) {
41 directory.deleteSync(recursive: true);
42 }
43 } on FileSystemException {
44 logger.printWarning('Failed to delete directory at: ${directory.path}');
45 return false;
46 }
47 return true;
48}
49
50/// Represents Flutter-specific data that is added to the `PUB_ENVIRONMENT`
51/// environment variable and allows understanding the type of requests made to
52/// the package site on Flutter's behalf.
53// DO NOT update without contacting kevmoo.
54// We have server-side tooling that assumes the values are consistent.
55class PubContext {
56 PubContext._(this._values) {
57 for (final String item in _values) {
58 if (!_validContext.hasMatch(item)) {
59 throw ArgumentError.value(_values, 'value', 'Must match RegExp ${_validContext.pattern}');
60 }
61 }
62 }
63
64 static PubContext getVerifyContext(String commandName) =>
65 PubContext._(<String>['verify', commandName.replaceAll('-', '_')]);
66
67 static final create = PubContext._(<String>['create']);
68 static final createPackage = PubContext._(<String>['create_pkg']);
69 static final createPlugin = PubContext._(<String>['create_plugin']);
70 static final interactive = PubContext._(<String>['interactive']);
71 static final pubGet = PubContext._(<String>['get']);
72 static final pubUpgrade = PubContext._(<String>['upgrade']);
73 static final pubAdd = PubContext._(<String>['add']);
74 static final pubRemove = PubContext._(<String>['remove']);
75 static final pubForward = PubContext._(<String>['forward']);
76 static final pubPassThrough = PubContext._(<String>['passthrough']);
77 static final runTest = PubContext._(<String>['run_test']);
78 static final flutterTests = PubContext._(<String>['flutter_tests']);
79 static final updatePackages = PubContext._(<String>['update_packages']);
80
81 final List<String> _values;
82
83 static final _validContext = RegExp('[a-z][a-z_]*[a-z]');
84
85 @override
86 String toString() => 'PubContext: ${_values.join(':')}';
87
88 String toAnalyticsString() {
89 return _values.map((String s) => s.replaceAll('_', '-')).toList().join('-');
90 }
91}
92
93/// Describes the amount of output that should get printed from a `pub` command.
94enum PubOutputMode {
95 /// No normal output should be printed.
96 ///
97 /// If the command were to fail, failures are still printed.
98 failuresOnly,
99
100 /// The complete output should be printed; this is typically the default.
101 all,
102
103 /// Only summary information should be printed.
104 summaryOnly,
105}
106
107/// A handle for interacting with the pub tool.
108abstract class Pub {
109 /// Create a default [Pub] instance.
110 factory Pub({
111 required FileSystem fileSystem,
112 required Logger logger,
113 required ProcessManager processManager,
114 required Platform platform,
115 required BotDetector botDetector,
116 }) = _DefaultPub;
117
118 /// Create a [Pub] instance with a mocked [stdio].
119 @visibleForTesting
120 factory Pub.test({
121 required FileSystem fileSystem,
122 required Logger logger,
123 required ProcessManager processManager,
124 required Platform platform,
125 required BotDetector botDetector,
126 required Stdio stdio,
127 }) = _DefaultPub.test;
128
129 /// Runs `pub get` for [project].
130 ///
131 /// [context] provides extra information to package server requests to
132 /// understand usage.
133 ///
134 /// If [shouldSkipThirdPartyGenerator] is true, the overall pub get will be
135 /// skipped if the package config file has a "generator" other than "pub".
136 /// Defaults to true.
137 ///
138 /// [outputMode] determines how detailed the output from `pub get` will be.
139 /// If [PubOutputMode.all] is used, `pub get` will print its typical output
140 /// which includes information about all changed dependencies. If
141 /// [PubOutputMode.summaryOnly] is used, only summary information will be printed.
142 /// This is useful for cases where the user is typically not interested in
143 /// what dependencies were changed, such as when running `flutter create`.
144 ///
145 /// Will also resolve dependencies in the example folder if present.
146 Future<void> get({
147 required PubContext context,
148 required FlutterProject project,
149 bool upgrade = false,
150 bool offline = false,
151 String? flutterRootOverride,
152 bool checkUpToDate = false,
153 bool shouldSkipThirdPartyGenerator = true,
154 bool enforceLockfile = false,
155 PubOutputMode outputMode = PubOutputMode.all,
156 });
157
158 /// Runs pub in 'batch' mode.
159 ///
160 /// forwarding complete lines written by pub to its stdout/stderr streams to
161 /// the corresponding stream of this process, optionally applying filtering.
162 /// The pub process will not receive anything on its stdin stream.
163 ///
164 /// The `--trace` argument is passed to `pub` when `showTraceForErrors`
165 /// `isRunningOnBot` is true.
166 ///
167 /// [context] provides extra information to package server requests to
168 /// understand usage.
169 Future<void> batch(
170 List<String> arguments, {
171 required PubContext context,
172 String? directory,
173 MessageFilter? filter,
174 String failureMessage = 'pub failed',
175 });
176
177 /// Runs pub in 'interactive' mode.
178 ///
179 /// This will run the pub process with StdioInherited (unless `stdio` is set
180 /// for testing).
181 ///
182 /// The pub process will be run in current working directory, so `--directory`
183 /// should be passed appropriately in [arguments]. This ensures output from
184 /// pub will refer to relative paths correctly.
185 ///
186 /// [touchesPackageConfig] should be true if this is a command expected to
187 /// create a new `.dart_tool/package_config.json` file.
188 Future<void> interactively(
189 List<String> arguments, {
190 FlutterProject? project,
191 required PubContext context,
192 required String command,
193 bool touchesPackageConfig = false,
194 PubOutputMode outputMode = PubOutputMode.all,
195 });
196}
197
198class _DefaultPub implements Pub {
199 _DefaultPub({
200 required FileSystem fileSystem,
201 required Logger logger,
202 required ProcessManager processManager,
203 required Platform platform,
204 required BotDetector botDetector,
205 }) : _fileSystem = fileSystem,
206 _logger = logger,
207 _platform = platform,
208 _botDetector = botDetector,
209 _processUtils = ProcessUtils(logger: logger, processManager: processManager),
210 _processManager = processManager,
211 _stdio = null;
212
213 @visibleForTesting
214 _DefaultPub.test({
215 required FileSystem fileSystem,
216 required Logger logger,
217 required ProcessManager processManager,
218 required Platform platform,
219 required BotDetector botDetector,
220 required Stdio stdio,
221 }) : _fileSystem = fileSystem,
222 _logger = logger,
223 _platform = platform,
224 _botDetector = botDetector,
225 _processUtils = ProcessUtils(logger: logger, processManager: processManager),
226 _processManager = processManager,
227 _stdio = stdio;
228
229 final FileSystem _fileSystem;
230 final Logger _logger;
231 final ProcessUtils _processUtils;
232 final Platform _platform;
233 final BotDetector _botDetector;
234 final ProcessManager _processManager;
235 final Stdio? _stdio;
236
237 @override
238 Future<void> get({
239 required PubContext context,
240 required FlutterProject project,
241 bool upgrade = false,
242 bool offline = false,
243 String? flutterRootOverride,
244 bool checkUpToDate = false,
245 bool shouldSkipThirdPartyGenerator = true,
246 bool enforceLockfile = false,
247 PubOutputMode outputMode = PubOutputMode.all,
248 }) async {
249 final String directory = project.directory.path;
250
251 // Here we use pub's private helper file to locate the package_config.
252 // In pub workspaces pub will generate a `.dart_tool/pub/workspace_ref.json`
253 // inside each workspace-package that refers to the workspace root where
254 // .dart_tool/package_config.json is located.
255 //
256 // By checking for this file instead of iterating parent directories until
257 // finding .dart_tool/package_config.json we will not mistakenly find a
258 // package_config.json from outside the workspace.
259 //
260 // TODO(sigurdm): avoid relying on pubs implementation details somehow?
261 final File workspaceRefFile = project.dartTool
262 .childDirectory('pub')
263 .childFile('workspace_ref.json');
264 final File packageConfigFile;
265 if (workspaceRefFile.existsSync()) {
266 switch (jsonDecode(workspaceRefFile.readAsStringSync())) {
267 case {'workspaceRoot': final String workspaceRoot}:
268 packageConfigFile = _fileSystem.file(
269 _fileSystem.path.join(workspaceRefFile.parent.path, workspaceRoot),
270 );
271 default:
272 // The workspace_ref.json file was malformed. Attempt to load the
273 // regular .dart_tool/package_config.json
274 //
275 // Most likely this doesn't exist, and we will get a new pub
276 // resolution.
277 //
278 // Alternatively this is a stray file somehow, and it can be ignored.
279 packageConfigFile = project.dartTool.childFile('package_config.json');
280 }
281 } else {
282 packageConfigFile = project.dartTool.childFile('package_config.json');
283 }
284
285 if (packageConfigFile.existsSync()) {
286 final Directory workspaceRoot = packageConfigFile.parent.parent;
287 final File lastVersion = workspaceRoot.childDirectory('.dart_tool').childFile('version');
288 final File currentVersion = _fileSystem.file(
289 _fileSystem.path.join(Cache.flutterRoot!, 'version'),
290 );
291 final File pubspecYaml = project.pubspecFile;
292 final File pubLockFile = workspaceRoot.childFile('pubspec.lock');
293
294 if (shouldSkipThirdPartyGenerator) {
295 Map<String, Object?> packageConfigMap;
296 try {
297 packageConfigMap =
298 jsonDecode(packageConfigFile.readAsStringSync()) as Map<String, Object?>;
299 } on FormatException {
300 packageConfigMap = <String, Object?>{};
301 }
302
303 final bool isPackageConfigGeneratedByThirdParty =
304 packageConfigMap.containsKey('generator') && packageConfigMap['generator'] != 'pub';
305
306 if (isPackageConfigGeneratedByThirdParty) {
307 _logger.printTrace('Skipping pub get: generated by third-party.');
308 return;
309 }
310 }
311
312 // If the pubspec.yaml is older than the package config file and the last
313 // flutter version used is the same as the current version skip pub get.
314 // This will incorrectly skip pub on the master branch if dependencies
315 // are being added/removed from the flutter framework packages, but this
316 // can be worked around by manually running pub.
317 if (checkUpToDate &&
318 pubLockFile.existsSync() &&
319 pubspecYaml.lastModifiedSync().isBefore(pubLockFile.lastModifiedSync()) &&
320 pubspecYaml.lastModifiedSync().isBefore(packageConfigFile.lastModifiedSync()) &&
321 lastVersion.existsSync() &&
322 lastVersion.readAsStringSync() == currentVersion.readAsStringSync()) {
323 _logger.printTrace('Skipping pub get: version match.');
324 return;
325 }
326 }
327
328 final command = upgrade ? 'upgrade' : 'get';
329 final args = <String>[
330 if (_logger.supportsColor) '--color',
331 '--directory',
332 _fileSystem.path.relative(directory),
333 ...<String>[command],
334 if (offline) '--offline',
335 '--example',
336 if (enforceLockfile) '--enforce-lockfile',
337 ];
338 await _runWithStdioInherited(
339 args,
340 command: command,
341 context: context,
342 directory: directory,
343 failureMessage: 'pub $command failed',
344 flutterRootOverride: flutterRootOverride,
345 outputMode: outputMode,
346 );
347 await _updateVersionAndPackageConfig(project);
348 }
349
350 /// Runs pub with [arguments] and [ProcessStartMode.inheritStdio] mode.
351 ///
352 /// Uses [ProcessStartMode.normal] and [_stdio] if [Pub.test] constructor
353 /// was used.
354 ///
355 /// Prints the stdout and stderr of the whole run, unless silenced using
356 /// [outputMode].
357 ///
358 /// Sends an analytics event.
359 Future<void> _runWithStdioInherited(
360 List<String> arguments, {
361 required String command,
362 required PubOutputMode outputMode,
363 required PubContext context,
364 required String directory,
365 String failureMessage = 'pub failed',
366 String? flutterRootOverride,
367 }) async {
368 int exitCode;
369
370 final pubCommand = <String>[..._pubCommand, ...arguments];
371 final Map<String, String> pubEnvironment = await _createPubEnvironment(
372 context: context,
373 flutterRootOverride: flutterRootOverride,
374 summaryOnly: outputMode == PubOutputMode.summaryOnly,
375 );
376
377 String? pubStderr;
378 try {
379 if (outputMode != PubOutputMode.failuresOnly) {
380 final io.Stdio? stdio = _stdio;
381 if (stdio == null) {
382 // Let pub inherit stdio and output directly to the tool's stdout and
383 // stderr handles.
384 final io.Process process = await _processUtils.start(
385 pubCommand,
386 workingDirectory: _fileSystem.path.current,
387 environment: pubEnvironment,
388 mode: ProcessStartMode.inheritStdio,
389 );
390
391 exitCode = await process.exitCode;
392 } else {
393 // Omit [mode] parameter to send output to [process.stdout] and
394 // [process.stderr].
395 final io.Process process = await _processUtils.start(
396 pubCommand,
397 workingDirectory: _fileSystem.path.current,
398 environment: pubEnvironment,
399 );
400
401 // Direct pub output to [Pub._stdio] for tests.
402 final StreamSubscription<List<int>> stdoutSubscription = process.stdout.listen(
403 stdio.stdout.add,
404 );
405 final StreamSubscription<List<int>> stderrSubscription = process.stderr.listen(
406 stdio.stderr.add,
407 );
408
409 await Future.wait<void>(<Future<void>>[
410 stdoutSubscription.asFuture<void>(),
411 stderrSubscription.asFuture<void>(),
412 ]);
413
414 unawaited(stdoutSubscription.cancel());
415 unawaited(stderrSubscription.cancel());
416
417 exitCode = await process.exitCode;
418 }
419 } else {
420 // Do not try to use [ProcessUtils.start] here, because it requires you
421 // to read all data out of the stdout and stderr streams. If you don't
422 // read the streams, it may appear to work fine on your platform but
423 // will block the tool's process on Windows.
424 // See https://api.dart.dev/stable/dart-io/Process/start.html
425 //
426 // [ProcessUtils.run] will send the output to [result.stdout] and
427 // [result.stderr], which we will ignore.
428 final RunResult result = await _processUtils.run(
429 pubCommand,
430 workingDirectory: _fileSystem.path.current,
431 environment: pubEnvironment,
432 );
433
434 exitCode = result.exitCode;
435 pubStderr = result.stderr;
436 }
437 } on io.ProcessException catch (exception) {
438 final buffer = StringBuffer('${exception.message}\n');
439 final directoryExistsMessage = _fileSystem.directory(directory).existsSync()
440 ? 'exists'
441 : 'does not exist';
442 buffer.writeln('Working directory: "$directory" ($directoryExistsMessage)');
443 buffer.write(_stringifyPubEnv(pubEnvironment));
444 throw io.ProcessException(
445 exception.executable,
446 exception.arguments,
447 buffer.toString(),
448 exception.errorCode,
449 );
450 }
451
452 final code = exitCode;
453
454 if (code != 0) {
455 final buffer = StringBuffer('$failureMessage\n');
456 buffer.writeln('command: "${pubCommand.join(' ')}"');
457 buffer.write(_stringifyPubEnv(pubEnvironment));
458 buffer.writeln('exit code: $code');
459 _logger.printTrace(buffer.toString());
460
461 // When this is null, but a failure happened, it is assumed that stderr
462 // was already redirected to the process stderr. This handles the corner
463 // case where we otherwise would log nothing. See
464 // https://github.com/flutter/flutter/issues/148569 for details.
465 if (pubStderr != null) {
466 _logger.printError(pubStderr);
467 }
468 if (context == PubContext.updatePackages) {
469 _logger.printWarning(
470 'If the current version was resolved as $kUnknownFrameworkVersion '
471 'and this is a fork of flutter/flutter, you forgot to set the remote '
472 'upstream branch to point to the canonical flutter/flutter: \n\n'
473 ' git remote set-url upstream https://github.com/flutter/flutter.git\n'
474 '\n'
475 'See https://github.com/flutter/flutter/blob/main/docs/contributing/Setting-up-the-Framework-development-environment.md#set-up-your-environment.',
476 );
477 }
478 throwToolExit('Failed to update packages.', exitCode: code);
479 }
480 }
481
482 // For surfacing pub env in crash reporting
483 String _stringifyPubEnv(Map<String, String> map, {String prefix = 'pub env'}) {
484 if (map.isEmpty) {
485 return '';
486 }
487 final buffer = StringBuffer();
488 buffer.writeln('$prefix: {');
489 for (final MapEntry<String, String> entry in map.entries) {
490 buffer.writeln(' "${entry.key}": "${entry.value}",');
491 }
492 buffer.writeln('}');
493 return buffer.toString();
494 }
495
496 @override
497 Future<void> batch(
498 List<String> arguments, {
499 required PubContext context,
500 String? directory,
501 MessageFilter? filter,
502 String failureMessage = 'pub failed',
503 String? flutterRootOverride,
504 }) async {
505 final bool showTraceForErrors = await _botDetector.isRunningOnBot;
506
507 var lastPubMessage = 'no message';
508 String? filterWrapper(String line) {
509 lastPubMessage = line;
510 if (filter == null) {
511 return line;
512 }
513 return filter(line);
514 }
515
516 if (showTraceForErrors) {
517 arguments.insert(0, '--trace');
518 }
519 final Map<String, String> pubEnvironment = await _createPubEnvironment(
520 context: context,
521 flutterRootOverride: flutterRootOverride,
522 );
523 final pubCommand = <String>[..._pubCommand, ...arguments];
524 final int code = await _processUtils.stream(
525 pubCommand,
526 workingDirectory: directory,
527 mapFunction: filterWrapper, // may set versionSolvingFailed, lastPubMessage
528 environment: pubEnvironment,
529 );
530
531 if (code != 0) {
532 final buffer = StringBuffer('$failureMessage\n');
533 buffer.writeln('command: "${pubCommand.join(' ')}"');
534 buffer.write(_stringifyPubEnv(pubEnvironment));
535 buffer.writeln('exit code: $code');
536 buffer.writeln('last line of pub output: "${lastPubMessage.trim()}"');
537 throwToolExit(buffer.toString(), exitCode: code);
538 }
539 }
540
541 @override
542 Future<void> interactively(
543 List<String> arguments, {
544 FlutterProject? project,
545 required PubContext context,
546 required String command,
547 bool touchesPackageConfig = false,
548 bool generateSyntheticPackage = false,
549 PubOutputMode outputMode = PubOutputMode.all,
550 }) async {
551 await _runWithStdioInherited(
552 arguments,
553 command: command,
554 directory: _fileSystem.currentDirectory.path,
555 context: context,
556 outputMode: outputMode,
557 );
558 if (touchesPackageConfig && project != null) {
559 await _updateVersionAndPackageConfig(project);
560 }
561 }
562
563 /// The command used for running pub.
564 late final List<String> _pubCommand = _computePubCommand();
565
566 List<String> _computePubCommand() {
567 // TODO(zanderso): refactor to use artifacts.
568 final String sdkPath = _fileSystem.path.joinAll(<String>[
569 Cache.flutterRoot!,
570 'bin',
571 'cache',
572 'dart-sdk',
573 'bin',
574 'dart',
575 ]);
576 if (!_processManager.canRun(sdkPath)) {
577 throwToolExit(
578 'Your Flutter SDK download may be corrupt or missing permissions to run. '
579 'Try re-downloading the Flutter SDK into a directory that has read/write '
580 'permissions for the current user.',
581 );
582 }
583 return <String>[sdkPath, 'pub', '--suppress-analytics'];
584 }
585
586 // Returns the environment value that should be used when running pub.
587 //
588 // Includes any existing environment variable, if one exists.
589 //
590 // [context] provides extra information to package server requests to
591 // understand usage.
592 Future<String> _getPubEnvironmentValue(PubContext pubContext) async {
593 // DO NOT update this function without contacting kevmoo.
594 // We have server-side tooling that assumes the values are consistent.
595 final String? existing = _platform.environment[_kPubEnvironmentKey];
596 final values = <String>[
597 if (existing != null && existing.isNotEmpty) existing,
598 if (await _botDetector.isRunningOnBot) 'flutter_bot',
599 'flutter_cli',
600 ...pubContext._values,
601 ];
602 return values.join(':');
603 }
604
605 /// There are 2 ways to get the pub cache location
606 ///
607 /// 1) Provide the _kPubCacheEnvironmentKey.
608 /// 2) The pub default user-level pub cache.
609 ///
610 /// If we are using 2, check if there are pre-packaged packages in
611 /// $FLUTTER_ROOT/.pub-preload-cache and install them in the user-level cache.
612 String? _getPubCacheIfAvailable() {
613 if (_platform.environment.containsKey(_kPubCacheEnvironmentKey)) {
614 return _platform.environment[_kPubCacheEnvironmentKey];
615 }
616 _preloadPubCache();
617 // Use pub's default location by returning null.
618 return null;
619 }
620
621 /// Load any package-files stored in `FLUTTER_ROOT/.pub-preload-cache` into
622 /// the pub cache if it exists.
623 ///
624 /// Deletes the `.pub-preload-cache` directory.
625 void _preloadPubCache() {
626 final String flutterRootPath = Cache.flutterRoot!;
627 final Directory flutterRoot = _fileSystem.directory(flutterRootPath);
628 final Directory preloadCacheDir = flutterRoot.childDirectory('.pub-preload-cache');
629 if (preloadCacheDir.existsSync()) {
630 /// We only want to inform about existing caches on first run of a freshly
631 /// downloaded Flutter SDK. Therefore it is conditioned on the existence
632 /// of the .pub-preload-cache dir.
633 final Iterable<String> cacheFiles = preloadCacheDir
634 .listSync()
635 .map((FileSystemEntity f) => f.path)
636 .where((String path) => path.endsWith('.tar.gz'));
637 _processManager.runSync(<String>[..._pubCommand, 'cache', 'preload', ...cacheFiles]);
638 _tryDeleteDirectory(preloadCacheDir, _logger);
639 }
640 }
641
642 /// The full environment used when running pub.
643 ///
644 /// [context] provides extra information to package server requests to
645 /// understand usage.
646 Future<Map<String, String>> _createPubEnvironment({
647 required PubContext context,
648 String? flutterRootOverride,
649 bool? summaryOnly = false,
650 }) async {
651 final environment = <String, String>{
652 'FLUTTER_ROOT': flutterRootOverride ?? Cache.flutterRoot!,
653 _kPubEnvironmentKey: await _getPubEnvironmentValue(context),
654 if (summaryOnly ?? false) 'PUB_SUMMARY_ONLY': '1',
655 };
656 final String? pubCache = _getPubCacheIfAvailable();
657 if (pubCache != null) {
658 environment[_kPubCacheEnvironmentKey] = pubCache;
659 }
660 return environment;
661 }
662
663 /// Updates the .dart_tool/version file to be equal to current Flutter
664 /// version.
665 ///
666 /// This should be called after pub invocations that are expected to update
667 /// the packageConfig.
668 Future<void> _updateVersionAndPackageConfig(FlutterProject project) async {
669 final File? packageConfig = findPackageConfigFile(project.directory);
670 if (packageConfig == null) {
671 throwToolExit(
672 '${project.directory}: pub did not create .dart_tools/package_config.json file.',
673 );
674 }
675 final File lastVersion = _fileSystem.file(
676 _fileSystem.path.join(packageConfig.parent.path, 'version'),
677 );
678 final File currentVersion = _fileSystem.file(
679 _fileSystem.path.join(Cache.flutterRoot!, 'version'),
680 );
681 lastVersion.writeAsStringSync(currentVersion.readAsStringSync());
682
683 if (project.hasExampleApp && project.example.pubspecFile.existsSync()) {
684 final File? examplePackageConfig = findPackageConfigFile(project.example.directory);
685 if (examplePackageConfig == null) {
686 throwToolExit(
687 '${project.directory}: pub did not create example/.dart_tools/package_config.json file.',
688 );
689 }
690 }
691 }
692}
693