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