// Copyright 2014 The Flutter Authors. 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:async'; import 'package:meta/meta.dart'; import '../base/common.dart'; import '../base/context.dart'; import '../base/file_system.dart'; import '../base/io.dart' as io; import '../base/logger.dart'; import '../base/process.dart'; import '../cache.dart'; import '../globals.dart' as globals; import '../reporting/reporting.dart'; import '../runner/flutter_command.dart'; import 'sdk.dart'; /// The [Pub] instance. Pub get pub => context.get(); /// 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 { PubContext._(this._values) { for (final String item in _values) { if (!_validContext.hasMatch(item)) { throw ArgumentError.value( _values, 'value', 'Must match RegExp ${_validContext.pattern}'); } } } static PubContext getVerifyContext(String commandName) => PubContext._(['verify', commandName.replaceAll('-', '_')]); static final PubContext create = PubContext._(['create']); static final PubContext createPackage = PubContext._(['create_pkg']); static final PubContext createPlugin = PubContext._(['create_plugin']); static final PubContext interactive = PubContext._(['interactive']); static final PubContext pubGet = PubContext._(['get']); static final PubContext pubUpgrade = PubContext._(['upgrade']); static final PubContext pubForward = PubContext._(['forward']); static final PubContext runTest = PubContext._(['run_test']); static final PubContext flutterTests = PubContext._(['flutter_tests']); static final PubContext updatePackages = PubContext._(['update_packages']); final List _values; static final RegExp _validContext = RegExp('[a-z][a-z_]*[a-z]'); @override String toString() => 'PubContext: ${_values.join(':')}'; String toAnalyticsString() { return _values.map((String s) => s.replaceAll('_', '-')).toList().join('-'); } } bool _shouldRunPubGet({ File pubSpecYaml, File dotPackages }) { if (!dotPackages.existsSync()) { return true; } final DateTime dotPackagesLastModified = dotPackages.lastModifiedSync(); if (pubSpecYaml.lastModifiedSync().isAfter(dotPackagesLastModified)) { return true; } final File flutterToolsStamp = globals.cache.getStampFileFor('flutter_tools'); if (flutterToolsStamp.existsSync() && flutterToolsStamp.lastModifiedSync().isAfter(dotPackagesLastModified)) { return true; } return false; } /// A handle for interacting with the pub tool. abstract class Pub { /// Create a default [Pub] instance. const factory Pub() = _DefaultPub; /// Runs `pub get`. /// /// [context] provides extra information to package server requests to /// understand usage. Future get({ @required PubContext context, String directory, bool skipIfAbsent = false, bool upgrade = false, bool offline = false, bool checkLastModified = true, bool skipPubspecYamlCheck = false, }); /// 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 batch( List arguments, { @required PubContext context, String directory, MessageFilter filter, String failureMessage = 'pub failed', @required bool retry, bool showTraceForErrors, }); /// 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 interactively( List arguments, { String directory, }); } class _DefaultPub implements Pub { const _DefaultPub(); @override Future get({ @required PubContext context, String directory, bool skipIfAbsent = false, bool upgrade = false, bool offline = false, bool checkLastModified = true, bool skipPubspecYamlCheck = false, }) async { directory ??= globals.fs.currentDirectory.path; final File pubSpecYaml = globals.fs.file(globals.fs.path.join(directory, 'pubspec.yaml')); final File dotPackages = globals.fs.file(globals.fs.path.join(directory, '.packages')); if (!skipPubspecYamlCheck && !pubSpecYaml.existsSync()) { if (!skipIfAbsent) { throwToolExit('$directory: no pubspec.yaml found'); } return; } final DateTime originalPubspecYamlModificationTime = pubSpecYaml.lastModifiedSync(); if (!checkLastModified || _shouldRunPubGet(pubSpecYaml: pubSpecYaml, dotPackages: dotPackages)) { final String command = upgrade ? 'upgrade' : 'get'; final Status status = globals.logger.startProgress( 'Running "flutter pub $command" in ${globals.fs.path.basename(directory)}...', timeout: timeoutConfiguration.slowOperation, ); final bool verbose = FlutterCommand.current != null && FlutterCommand.current.globalResults['verbose'] as bool; final List args = [ if (verbose) '--verbose' else '--verbosity=warning', ...[command, '--no-precompile'], if (offline) '--offline', ]; try { await batch( args, context: context, directory: directory, filter: _filterOverrideWarnings, failureMessage: 'pub $command failed', retry: true, ); status.stop(); // The exception is rethrown, so don't catch only Exceptions. } catch (exception) { // ignore: avoid_catches_without_on_clauses status.cancel(); rethrow; } } if (!dotPackages.existsSync()) { throwToolExit('$directory: pub did not create .packages file.'); } if (pubSpecYaml.lastModifiedSync() != originalPubspecYamlModificationTime) { throwToolExit('$directory: unexpected concurrent modification of pubspec.yaml while running pub.'); } // We don't check if dotPackages was actually modified, because as far as we can tell sometimes // pub will decide it does not need to actually modify it. // Since we rely on the file having a more recent timestamp, though, we do manually force the // file to be more recently modified. final DateTime now = DateTime.now(); if (now.isBefore(originalPubspecYamlModificationTime)) { globals.printError( 'Warning: File "${globals.fs.path.absolute(pubSpecYaml.path)}" was created in the future. ' 'Optimizations that rely on comparing time stamps will be unreliable. Check your ' 'system clock for accuracy.\n' 'The timestamp was: $originalPubspecYamlModificationTime\n' 'The time now is: $now' ); } else { dotPackages.setLastModifiedSync(now); final DateTime newDotPackagesTimestamp = dotPackages.lastModifiedSync(); if (newDotPackagesTimestamp.isBefore(originalPubspecYamlModificationTime)) { globals.printError( 'Warning: Failed to set timestamp of "${globals.fs.path.absolute(dotPackages.path)}". ' 'Tried to set timestamp to $now, but new timestamp is $newDotPackagesTimestamp.' ); if (newDotPackagesTimestamp.isAfter(now)) { globals.printError('Maybe the file was concurrently modified?'); } } } } @override Future batch( List arguments, { @required PubContext context, String directory, MessageFilter filter, String failureMessage = 'pub failed', @required bool retry, bool showTraceForErrors, }) async { showTraceForErrors ??= await globals.isRunningOnBot; String lastPubMessage = 'no message'; bool versionSolvingFailed = false; String filterWrapper(String line) { lastPubMessage = line; if (line.contains('version solving failed')) { versionSolvingFailed = true; } if (filter == null) { return line; } return filter(line); } if (showTraceForErrors) { arguments.insert(0, '--trace'); } int attempts = 0; int duration = 1; int code; loop: while (true) { attempts += 1; code = await processUtils.stream( _pubCommand(arguments), workingDirectory: directory, mapFunction: filterWrapper, // may set versionSolvingFailed, lastPubMessage environment: await _createPubEnvironment(context), ); String message; switch (code) { case 69: // UNAVAILABLE in https://github.com/dart-lang/pub/blob/master/lib/src/exit_codes.dart message = 'server unavailable'; break; default: break loop; } assert(message != null); versionSolvingFailed = false; globals.printStatus('$failureMessage ($message) -- attempting retry $attempts in $duration second${ duration == 1 ? "" : "s"}...'); await Future.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, ).send(); if (code != 0) { throwToolExit('$failureMessage ($code; $lastPubMessage)', exitCode: code); } } @override Future interactively( List arguments, { String directory, }) async { Cache.releaseLockEarly(); final io.Process process = await processUtils.start( _pubCommand(arguments), workingDirectory: directory, environment: await _createPubEnvironment(PubContext.interactive), ); // Pipe the Flutter tool stdin to the pub stdin. unawaited(process.stdin.addStream(globals.stdio.stdin) // 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) { globals.printTrace('Echoing stdin to the pub subprocess failed:'); globals.printTrace('$err\n$stack'); } )); // Pipe the pub stdout and stderr to the tool stdout and stderr. try { await Future.wait(>[ globals.stdio.addStdoutStream(process.stdout), globals.stdio.addStderrStream(process.stderr), ]); } on Exception catch (err, stack) { globals.printTrace('Echoing stdout or stderr from the pub subprocess failed:'); globals.printTrace('$err\n$stack'); } // Wait for pub to exit. final int code = await process.exitCode; if (code != 0) { throwToolExit('pub finished with exit code $code', exitCode: code); } } /// The command used for running pub. List _pubCommand(List arguments) { return [sdkBinaryName('pub'), ...arguments]; } } typedef MessageFilter = String Function(String message); /// The full environment used when running pub. /// /// [context] provides extra information to package server requests to /// understand usage. Future> _createPubEnvironment(PubContext context) async { final Map environment = { 'FLUTTER_ROOT': Cache.flutterRoot, _pubEnvironmentKey: await _getPubEnvironmentValue(context), }; final String pubCache = _getRootPubCacheIfAvailable(); if (pubCache != null) { environment[_pubCacheEnvironmentKey] = pubCache; } return environment; } final RegExp _analyzerWarning = RegExp(r'^! \w+ [^ ]+ from path \.\./\.\./bin/cache/dart-sdk/lib/\w+$'); /// The console environment key used by the pub tool. const String _pubEnvironmentKey = 'PUB_ENVIRONMENT'; /// The console environment key used by the pub tool to find the cache directory. const String _pubCacheEnvironmentKey = 'PUB_CACHE'; /// 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 _getPubEnvironmentValue(PubContext pubContext) async { // DO NOT update this function without contacting kevmoo. // We have server-side tooling that assumes the values are consistent. final String existing = globals.platform.environment[_pubEnvironmentKey]; final List values = [ if (existing != null && existing.isNotEmpty) existing, if (await globals.isRunningOnBot) 'flutter_bot', 'flutter_cli', ...pubContext._values, ]; return values.join(':'); } String _getRootPubCacheIfAvailable() { if (globals.platform.environment.containsKey(_pubCacheEnvironmentKey)) { return globals.platform.environment[_pubCacheEnvironmentKey]; } final String cachePath = globals.fs.path.join(Cache.flutterRoot, '.pub-cache'); if (globals.fs.directory(cachePath).existsSync()) { globals.printTrace('Using $cachePath for the pub cache.'); return cachePath; } // Use pub's default location by returning null. return null; } String _filterOverrideWarnings(String message) { // This function filters out these three messages: // Warning: You are using these overridden dependencies: // ! analyzer 0.29.0-alpha.0 from path ../../bin/cache/dart-sdk/lib/analyzer // ! front_end 0.1.0-alpha.0 from path ../../bin/cache/dart-sdk/lib/front_end if (message == 'Warning: You are using these overridden dependencies:') { return null; } if (message.contains(_analyzerWarning)) { return null; } return message; }