pub.dart 19.8 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:async';

7
import 'package:package_config/package_config.dart';
8
import 'package:process/process.dart';
9

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/logger.dart';
16
import '../base/platform.dart';
17
import '../base/process.dart';
18
import '../cache.dart';
19
import '../convert.dart';
20
import '../dart/package_map.dart';
21
import '../reporting/reporting.dart';
22

23
/// The [Pub] instance.
24
Pub get pub => context.get<Pub>()!;
25

26 27 28 29 30 31
/// The console environment key used by the pub tool.
const String _kPubEnvironmentKey = 'PUB_ENVIRONMENT';

/// The console environment key used by the pub tool to find the cache directory.
const String _kPubCacheEnvironmentKey = 'PUB_CACHE';

32 33 34 35
/// The UNAVAILABLE exit code returned by the pub tool.
/// (see https://github.com/dart-lang/pub/blob/master/lib/src/exit_codes.dart)
const int _kPubExitCodeUnavailable = 69;

36
typedef MessageFilter = String? Function(String message);
37

38 39 40 41 42 43
/// Represents Flutter-specific data that is added to the `PUB_ENVIRONMENT`
/// environment variable and allows understanding the type of requests made to
/// the package site on Flutter's behalf.
// DO NOT update without contacting kevmoo.
// We have server-side tooling that assumes the values are consistent.
class PubContext {
44
  PubContext._(this._values) {
45
    for (final String item in _values) {
46 47
      if (!_validContext.hasMatch(item)) {
        throw ArgumentError.value(
48
          _values, 'value', 'Must match RegExp ${_validContext.pattern}');
49 50 51 52 53 54
      }
    }
  }

  static PubContext getVerifyContext(String commandName) =>
      PubContext._(<String>['verify', commandName.replaceAll('-', '_')]);
55

56 57 58 59 60 61
  static final PubContext create = PubContext._(<String>['create']);
  static final PubContext createPackage = PubContext._(<String>['create_pkg']);
  static final PubContext createPlugin = PubContext._(<String>['create_plugin']);
  static final PubContext interactive = PubContext._(<String>['interactive']);
  static final PubContext pubGet = PubContext._(<String>['get']);
  static final PubContext pubUpgrade = PubContext._(<String>['upgrade']);
62
  static final PubContext pubForward = PubContext._(<String>['forward']);
63 64 65
  static final PubContext runTest = PubContext._(<String>['run_test']);
  static final PubContext flutterTests = PubContext._(<String>['flutter_tests']);
  static final PubContext updatePackages = PubContext._(<String>['update_packages']);
66 67 68

  final List<String> _values;

69
  static final RegExp _validContext = RegExp('[a-z][a-z_]*[a-z]');
70 71 72

  @override
  String toString() => 'PubContext: ${_values.join(':')}';
73 74 75 76

  String toAnalyticsString()  {
    return _values.map((String s) => s.replaceAll('_', '-')).toList().join('-');
  }
77 78
}

79 80 81
/// A handle for interacting with the pub tool.
abstract class Pub {
  /// Create a default [Pub] instance.
82
  factory Pub({
83 84 85 86 87 88
    required FileSystem fileSystem,
    required Logger logger,
    required ProcessManager processManager,
    required Platform platform,
    required BotDetector botDetector,
    required Usage usage,
89
  }) = _DefaultPub;
90 91 92 93 94

  /// Runs `pub get`.
  ///
  /// [context] provides extra information to package server requests to
  /// understand usage.
95 96 97 98
  ///
  /// If [shouldSkipThirdPartyGenerator] is true, the overall pub get will be
  /// skipped if the package config file has a "generator" other than "pub".
  /// Defaults to true.
99
  Future<void> get({
100
    required PubContext context,
101
    String? directory,
102 103 104
    bool skipIfAbsent = false,
    bool upgrade = false,
    bool offline = false,
105
    bool generateSyntheticPackage = false,
106
    String? flutterRootOverride,
107
    bool checkUpToDate = false,
108
    bool shouldSkipThirdPartyGenerator = true,
109
    bool printProgress = true,
110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125
  });

  /// Runs pub in 'batch' mode.
  ///
  /// forwarding complete lines written by pub to its stdout/stderr streams to
  /// the corresponding stream of this process, optionally applying filtering.
  /// The pub process will not receive anything on its stdin stream.
  ///
  /// The `--trace` argument is passed to `pub` (by mutating the provided
  /// `arguments` list) when `showTraceForErrors` is true, and when `showTraceForErrors`
  /// is null/unset, and `isRunningOnBot` is true.
  ///
  /// [context] provides extra information to package server requests to
  /// understand usage.
  Future<void> batch(
    List<String> arguments, {
126
    required PubContext context,
127 128
    String? directory,
    MessageFilter? filter,
129
    String failureMessage = 'pub failed',
130
    required bool retry,
131
    bool? showTraceForErrors,
132
  });
133 134 135 136 137 138 139

  /// Runs pub in 'interactive' mode.
  ///
  /// directly piping the stdin stream of this process to that of pub, and the
  /// stdout/stderr stream of pub to the corresponding streams of this process.
  Future<void> interactively(
    List<String> arguments, {
140
    String? directory,
141
    required io.Stdio stdio,
142 143
    bool touchesPackageConfig = false,
    bool generateSyntheticPackage = false,
144
  });
145 146 147
}

class _DefaultPub implements Pub {
148
  _DefaultPub({
149 150 151 152 153 154
    required FileSystem fileSystem,
    required Logger logger,
    required ProcessManager processManager,
    required Platform platform,
    required BotDetector botDetector,
    required Usage usage,
155
  }) : _fileSystem = fileSystem,
156 157 158 159 160 161 162
       _logger = logger,
       _platform = platform,
       _botDetector = botDetector,
       _usage = usage,
       _processUtils = ProcessUtils(
         logger: logger,
         processManager: processManager,
163 164
       ),
       _processManager = processManager;
165 166 167 168 169 170 171

  final FileSystem _fileSystem;
  final Logger _logger;
  final ProcessUtils _processUtils;
  final Platform _platform;
  final BotDetector _botDetector;
  final Usage _usage;
172
  final ProcessManager _processManager;
173 174 175

  @override
  Future<void> get({
176 177
    required PubContext context,
    String? directory,
178 179 180
    bool skipIfAbsent = false,
    bool upgrade = false,
    bool offline = false,
181
    bool generateSyntheticPackage = false,
182
    String? flutterRootOverride,
183
    bool checkUpToDate = false,
184
    bool shouldSkipThirdPartyGenerator = true,
185
    bool printProgress = true,
186
  }) async {
187 188 189
    directory ??= _fileSystem.currentDirectory.path;
    final File packageConfigFile = _fileSystem.file(
      _fileSystem.path.join(directory, '.dart_tool', 'package_config.json'));
190 191
    final Directory generatedDirectory = _fileSystem.directory(
      _fileSystem.path.join(directory, '.dart_tool', 'flutter_gen'));
192 193 194
    final File lastVersion = _fileSystem.file(
      _fileSystem.path.join(directory, '.dart_tool', 'version'));
    final File currentVersion = _fileSystem.file(
195
      _fileSystem.path.join(Cache.flutterRoot!, 'version'));
196 197 198 199 200 201
    final File pubspecYaml = _fileSystem.file(
      _fileSystem.path.join(directory, 'pubspec.yaml'));
    final File pubLockFile = _fileSystem.file(
      _fileSystem.path.join(directory, 'pubspec.lock')
    );

202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221
    if (shouldSkipThirdPartyGenerator && packageConfigFile.existsSync()) {
      Map<String, Object?> packageConfigMap;
      try {
        packageConfigMap = jsonDecode(
          packageConfigFile.readAsStringSync(),
        ) as Map<String, Object?>;
      } on FormatException {
        packageConfigMap = <String, Object?>{};
      }

      final bool isPackageConfigGeneratedByThirdParty =
          packageConfigMap.containsKey('generator') &&
          packageConfigMap['generator'] != 'pub';

      if (isPackageConfigGeneratedByThirdParty) {
        _logger.printTrace('Skipping pub get: generated by third-party.');
        return;
      }
    }

222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
    // If the pubspec.yaml is older than the package config file and the last
    // flutter version used is the same as the current version skip pub get.
    // This will incorrectly skip pub on the master branch if dependencies
    // are being added/removed from the flutter framework packages, but this
    // can be worked around by manually running pub.
    if (checkUpToDate &&
        packageConfigFile.existsSync() &&
        pubLockFile.existsSync() &&
        pubspecYaml.lastModifiedSync().isBefore(pubLockFile.lastModifiedSync()) &&
        pubspecYaml.lastModifiedSync().isBefore(packageConfigFile.lastModifiedSync()) &&
        lastVersion.existsSync() &&
        lastVersion.readAsStringSync() == currentVersion.readAsStringSync()) {
      _logger.printTrace('Skipping pub get: version match.');
      return;
    }
237

238
    final String command = upgrade ? 'upgrade' : 'get';
239
    final Status? status = printProgress ? _logger.startProgress(
240
      'Running "flutter pub $command" in ${_fileSystem.path.basename(directory)}...',
241
    ) : null;
242 243 244
    final bool verbose = _logger.isVerbose;
    final List<String> args = <String>[
      if (verbose)
245 246 247
        '--verbose'
      else
        '--verbosity=warning',
248 249 250 251 252 253 254 255 256 257 258 259 260
      ...<String>[
        command,
        '--no-precompile',
      ],
      if (offline)
        '--offline',
    ];
    try {
      await batch(
        args,
        context: context,
        directory: directory,
        failureMessage: 'pub $command failed',
261
        retry: !offline,
262
        flutterRootOverride: flutterRootOverride,
263
      );
264
      status?.stop();
265 266
    // The exception is rethrown, so don't catch only Exceptions.
    } catch (exception) { // ignore: avoid_catches_without_on_clauses
267
      status?.cancel();
268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
      if (exception is io.ProcessException) {
        final StringBuffer buffer = StringBuffer(exception.message);
        buffer.writeln('Working directory: "$directory"');
        final Map<String, String> env = await _createPubEnvironment(context, flutterRootOverride);
        if (env.entries.isNotEmpty) {
          buffer.writeln('pub env: {');
          for (final MapEntry<String, String> entry in env.entries) {
            buffer.writeln('  "${entry.key}": "${entry.value}",');
          }
          buffer.writeln('}');
        }

        throw io.ProcessException(
          exception.executable,
          exception.arguments,
          buffer.toString(),
          exception.errorCode,
        );
      }
287
      rethrow;
288
    }
289

290
    if (!packageConfigFile.existsSync()) {
291
      throwToolExit('$directory: pub did not create .dart_tools/package_config.json file.');
292
    }
293
    lastVersion.writeAsStringSync(currentVersion.readAsStringSync());
294 295 296 297 298
    await _updatePackageConfig(
      packageConfigFile,
      generatedDirectory,
      generateSyntheticPackage,
    );
299
  }
300

301 302 303
  @override
  Future<void> batch(
    List<String> arguments, {
304 305 306
    required PubContext context,
    String? directory,
    MessageFilter? filter,
307
    String failureMessage = 'pub failed',
308 309 310
    required bool retry,
    bool? showTraceForErrors,
    String? flutterRootOverride,
311
  }) async {
312
    showTraceForErrors ??= await _botDetector.isRunningOnBot;
313

314
    String lastPubMessage = 'no message';
315
    bool versionSolvingFailed = false;
316
    String? filterWrapper(String line) {
317
      lastPubMessage = line;
318 319 320 321 322 323 324
      if (line.contains('version solving failed')) {
        versionSolvingFailed = true;
      }
      if (filter == null) {
        return line;
      }
      return filter(line);
325
    }
326 327 328

    if (showTraceForErrors) {
      arguments.insert(0, '--trace');
329
    }
330 331 332
    int attempts = 0;
    int duration = 1;
    int code;
333
    while (true) {
334
      attempts += 1;
335
      code = await _processUtils.stream(
336 337
        _pubCommand(arguments),
        workingDirectory: directory,
338
        mapFunction: filterWrapper, // may set versionSolvingFailed, lastPubMessage
339
        environment: await _createPubEnvironment(context, flutterRootOverride),
340
      );
341 342 343
      String? message;
      if (retry) {
        if (code == _kPubExitCodeUnavailable) {
344
          message = 'server unavailable';
345 346 347 348
        }
      }
      if (message == null) {
        break;
349 350
      }
      versionSolvingFailed = false;
351 352 353 354
      _logger.printStatus(
        '$failureMessage ($message) -- attempting retry $attempts in $duration '
        'second${ duration == 1 ? "" : "s"}...',
      );
355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
      await Future<void>.delayed(Duration(seconds: duration));
      if (duration < 64) {
        duration *= 2;
      }
    }
    assert(code != null);

    String result = 'success';
    if (versionSolvingFailed) {
      result = 'version-solving-failed';
    } else if (code != 0) {
      result = 'failure';
    }
    PubResultEvent(
      context: context.toAnalyticsString(),
      result: result,
371
      usage: _usage,
372
    ).send();
373

374
    if (code != 0) {
375
      throwToolExit('$failureMessage ($code; $lastPubMessage)', exitCode: code);
376
    }
377
  }
378 379 380 381

  @override
  Future<void> interactively(
    List<String> arguments, {
382 383
    String? directory,
    required io.Stdio stdio,
384 385
    bool touchesPackageConfig = false,
    bool generateSyntheticPackage = false,
386
  }) async {
387
    // Fully resolved pub or pub.bat is calculated based on current platform.
388 389 390 391 392
    final io.Process process = await _processUtils.start(
      _pubCommand(arguments),
      workingDirectory: directory,
      environment: await _createPubEnvironment(PubContext.interactive),
    );
393

394
    // Pipe the Flutter tool stdin to the pub stdin.
395
    unawaited(process.stdin.addStream(stdio.stdin)
396 397 398
      // If pub exits unexpectedly with an error, that will be reported below
      // by the tool exit after the exit code check.
      .catchError((dynamic err, StackTrace stack) {
399 400
        _logger.printTrace('Echoing stdin to the pub subprocess failed:');
        _logger.printTrace('$err\n$stack');
401 402 403 404 405 406
      }
    ));

    // Pipe the pub stdout and stderr to the tool stdout and stderr.
    try {
      await Future.wait<dynamic>(<Future<dynamic>>[
407 408
        stdio.addStdoutStream(process.stdout),
        stdio.addStderrStream(process.stderr),
409
      ]);
410
    } on Exception catch (err, stack) {
411 412
      _logger.printTrace('Echoing stdout or stderr from the pub subprocess failed:');
      _logger.printTrace('$err\n$stack');
413
    }
414 415 416 417 418 419

    // Wait for pub to exit.
    final int code = await process.exitCode;
    if (code != 0) {
      throwToolExit('pub finished with exit code $code', exitCode: code);
    }
420 421

    if (touchesPackageConfig) {
422
      final String targetDirectory = directory ?? _fileSystem.currentDirectory.path;
423
      final File packageConfigFile = _fileSystem.file(
424
        _fileSystem.path.join(targetDirectory, '.dart_tool', 'package_config.json'));
425
      final Directory generatedDirectory = _fileSystem.directory(
426
        _fileSystem.path.join(targetDirectory, '.dart_tool', 'flutter_gen'));
427
      final File lastVersion = _fileSystem.file(
428
        _fileSystem.path.join(targetDirectory, '.dart_tool', 'version'));
429
      final File currentVersion = _fileSystem.file(
430
        _fileSystem.path.join(Cache.flutterRoot!, 'version'));
431 432 433 434 435 436 437
        lastVersion.writeAsStringSync(currentVersion.readAsStringSync());
      await _updatePackageConfig(
        packageConfigFile,
        generatedDirectory,
        generateSyntheticPackage,
      );
    }
438
  }
439

440 441
  /// The command used for running pub.
  List<String> _pubCommand(List<String> arguments) {
442
    // TODO(zanderso): refactor to use artifacts.
443
    final String sdkPath = _fileSystem.path.joinAll(<String>[
444
      Cache.flutterRoot!,
445 446 447 448
      'bin',
      'cache',
      'dart-sdk',
      'bin',
449
      'dart',
450
    ]);
451 452 453 454 455 456 457
    if (!_processManager.canRun(sdkPath)) {
      throwToolExit(
        'Your Flutter SDK download may be corrupt or missing permissions to run. '
        'Try re-downloading the Flutter SDK into a directory that has read/write '
        'permissions for the current user.'
      );
    }
458
    return <String>[sdkPath, '__deprecated_pub', ...arguments];
459
  }
460

461 462 463 464 465 466 467 468 469
  // Returns the environment value that should be used when running pub.
  //
  // Includes any existing environment variable, if one exists.
  //
  // [context] provides extra information to package server requests to
  // understand usage.
  Future<String> _getPubEnvironmentValue(PubContext pubContext) async {
    // DO NOT update this function without contacting kevmoo.
    // We have server-side tooling that assumes the values are consistent.
470
    final String? existing = _platform.environment[_kPubEnvironmentKey];
471 472 473 474 475 476 477
    final List<String> values = <String>[
      if (existing != null && existing.isNotEmpty) existing,
      if (await _botDetector.isRunningOnBot) 'flutter_bot',
      'flutter_cli',
      ...pubContext._values,
    ];
    return values.join(':');
478 479
  }

480
  String? _getRootPubCacheIfAvailable() {
481 482 483
    if (_platform.environment.containsKey(_kPubCacheEnvironmentKey)) {
      return _platform.environment[_kPubCacheEnvironmentKey];
    }
484

485
    final String cachePath = _fileSystem.path.join(Cache.flutterRoot!, '.pub-cache');
486 487 488 489
    if (_fileSystem.directory(cachePath).existsSync()) {
      _logger.printTrace('Using $cachePath for the pub cache.');
      return cachePath;
    }
490

491
    // Use pub's default location by returning null.
492
    return null;
493
  }
494 495 496 497 498

  /// The full environment used when running pub.
  ///
  /// [context] provides extra information to package server requests to
  /// understand usage.
499
  Future<Map<String, String>> _createPubEnvironment(PubContext context, [ String? flutterRootOverride ]) async {
500
    final Map<String, String> environment = <String, String>{
501
      'FLUTTER_ROOT': flutterRootOverride ?? Cache.flutterRoot!,
502 503
      _kPubEnvironmentKey: await _getPubEnvironmentValue(context),
    };
504
    final String? pubCache = _getRootPubCacheIfAvailable();
505 506 507 508
    if (pubCache != null) {
      environment[_kPubCacheEnvironmentKey] = pubCache;
    }
    return environment;
509
  }
510

511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537
  /// Update the package configuration file.
  ///
  /// Creates a corresponding `package_config_subset` file that is used by the build
  /// system to avoid rebuilds caused by an updated pub timestamp.
  ///
  /// if [generateSyntheticPackage] is true then insert flutter_gen synthetic
  /// package into the package configuration. This is used by the l10n localization
  /// tooling to insert a new reference into the package_config file, allowing the import
  /// of a package URI that is not specified in the pubspec.yaml
  ///
  /// For more information, see:
  ///   * [generateLocalizations], `in lib/src/localizations/gen_l10n.dart`
  Future<void> _updatePackageConfig(
    File packageConfigFile,
    Directory generatedDirectory,
    bool generateSyntheticPackage,
  ) async {
    final PackageConfig packageConfig = await loadPackageConfigWithLogging(packageConfigFile, logger: _logger);

    packageConfigFile.parent
      .childFile('package_config_subset')
      .writeAsStringSync(_computePackageConfigSubset(
        packageConfig,
        _fileSystem,
      ));

    if (!generateSyntheticPackage) {
538 539 540 541 542
      return;
    }
    if (packageConfig.packages.any((Package package) => package.name == 'flutter_gen')) {
      return;
    }
543 544 545 546 547 548 549 550 551

    // TODO(jonahwillams): Using raw json manipulation here because
    // savePackageConfig always writes to local io, and it changes absolute
    // paths to relative on round trip.
    // See: https://github.com/dart-lang/package_config/issues/99,
    // and: https://github.com/dart-lang/package_config/issues/100.

    // Because [loadPackageConfigWithLogging] succeeded [packageConfigFile]
    // we can rely on the file to exist and be correctly formatted.
552 553
    final Map<String, dynamic> jsonContents =
        json.decode(packageConfigFile.readAsStringSync()) as Map<String, dynamic>;
554

555
    (jsonContents['packages'] as List<dynamic>).add(<String, dynamic>{
556 557 558 559 560 561
      'name': 'flutter_gen',
      'rootUri': 'flutter_gen',
      'languageVersion': '2.12',
    });

    packageConfigFile.writeAsStringSync(json.encode(jsonContents));
562
  }
563 564 565 566 567 568 569 570 571 572 573 574 575 576

  // Subset the package config file to only the parts that are relevant for
  // rerunning the dart compiler.
  String _computePackageConfigSubset(PackageConfig packageConfig, FileSystem fileSystem) {
    final StringBuffer buffer = StringBuffer();
    for (final Package package in packageConfig.packages) {
      buffer.writeln(package.name);
      buffer.writeln(package.languageVersion);
      buffer.writeln(package.root);
      buffer.writeln(package.packageUriRoot);
    }
    buffer.writeln(packageConfig.version);
    return buffer.toString();
  }
577
}