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

import 'dart:async';

7 8
import 'package:meta/meta.dart';

9
import '../base/common.dart';
10
import '../base/context.dart';
11
import '../base/file_system.dart';
12
import '../base/io.dart' as io;
13
import '../base/logger.dart';
14
import '../base/process.dart';
15
import '../cache.dart';
16
import '../globals.dart' as globals;
17
import '../reporting/reporting.dart';
18
import '../runner/flutter_command.dart';
19
import 'sdk.dart';
20

21 22 23
/// The [Pub] instance.
Pub get pub => context.get<Pub>();

24 25 26 27 28 29
/// 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 {
30
  PubContext._(this._values) {
31
    for (final String item in _values) {
32 33 34 35 36 37 38 39 40
      if (!_validContext.hasMatch(item)) {
        throw ArgumentError.value(
            _values, 'value', 'Must match RegExp ${_validContext.pattern}');
      }
    }
  }

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

42 43 44 45 46 47
  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']);
48
  static final PubContext pubForward = PubContext._(<String>['forward']);
49
  static final PubContext runTest = PubContext._(<String>['run_test']);
50

51 52
  static final PubContext flutterTests = PubContext._(<String>['flutter_tests']);
  static final PubContext updatePackages = PubContext._(<String>['update_packages']);
53 54 55

  final List<String> _values;

56
  static final RegExp _validContext = RegExp('[a-z][a-z_]*[a-z]');
57 58 59

  @override
  String toString() => 'PubContext: ${_values.join(':')}';
60 61 62 63

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

66
bool _shouldRunPubGet({ File pubSpecYaml, File dotPackages }) {
67
  if (!dotPackages.existsSync()) {
68
    return true;
69
  }
70
  final DateTime dotPackagesLastModified = dotPackages.lastModifiedSync();
71
  if (pubSpecYaml.lastModifiedSync().isAfter(dotPackagesLastModified)) {
72
    return true;
73
  }
74
  final File flutterToolsStamp = globals.cache.getStampFileFor('flutter_tools');
75
  if (flutterToolsStamp.existsSync() &&
76
      flutterToolsStamp.lastModifiedSync().isAfter(dotPackagesLastModified)) {
77
    return true;
78
  }
79 80 81
  return false;
}

82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
/// 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<void> 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<void> batch(
    List<String> arguments, {
115 116 117 118 119 120 121
    @required PubContext context,
    String directory,
    MessageFilter filter,
    String failureMessage = 'pub failed',
    @required bool retry,
    bool showTraceForErrors,
  });
122 123 124 125 126 127 128 129


  /// 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, {
130 131
    String directory,
  });
132 133 134 135 136 137 138 139 140 141 142 143 144 145 146
}

class _DefaultPub implements Pub {
  const _DefaultPub();

  @override
  Future<void> get({
    @required PubContext context,
    String directory,
    bool skipIfAbsent = false,
    bool upgrade = false,
    bool offline = false,
    bool checkLastModified = true,
    bool skipPubspecYamlCheck = false,
  }) async {
147
    directory ??= globals.fs.currentDirectory.path;
148

149 150
    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'));
151 152 153 154 155 156

    if (!skipPubspecYamlCheck && !pubSpecYaml.existsSync()) {
      if (!skipIfAbsent) {
        throwToolExit('$directory: no pubspec.yaml found');
      }
      return;
157
    }
158

159 160
    final DateTime originalPubspecYamlModificationTime = pubSpecYaml.lastModifiedSync();

161 162
    if (!checkLastModified || _shouldRunPubGet(pubSpecYaml: pubSpecYaml, dotPackages: dotPackages)) {
      final String command = upgrade ? 'upgrade' : 'get';
163 164
      final Status status = globals.logger.startProgress(
        'Running "flutter pub $command" in ${globals.fs.path.basename(directory)}...',
165
        timeout: timeoutConfiguration.slowOperation,
166
      );
167
      final bool verbose = FlutterCommand.current != null && FlutterCommand.current.globalResults['verbose'] as bool;
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182
      final List<String> args = <String>[
        if (verbose) '--verbose' else '--verbosity=warning',
        ...<String>[command, '--no-precompile'],
        if (offline) '--offline',
      ];
      try {
        await batch(
          args,
          context: context,
          directory: directory,
          filter: _filterOverrideWarnings,
          failureMessage: 'pub $command failed',
          retry: true,
        );
        status.stop();
183 184
      // The exception is rethrown, so don't catch only Exceptions.
      } catch (exception) { // ignore: avoid_catches_without_on_clauses
185 186 187
        status.cancel();
        rethrow;
      }
188
    }
189

190 191 192
    if (!dotPackages.existsSync()) {
      throwToolExit('$directory: pub did not create .packages file.');
    }
193 194 195 196 197 198 199 200 201
    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)) {
202 203
      globals.printError(
        'Warning: File "${globals.fs.path.absolute(pubSpecYaml.path)}" was created in the future. '
204 205 206 207 208 209 210 211 212
        '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)) {
213 214
        globals.printError(
          'Warning: Failed to set timestamp of "${globals.fs.path.absolute(dotPackages.path)}". '
215 216 217
          'Tried to set timestamp to $now, but new timestamp is $newDotPackagesTimestamp.'
        );
        if (newDotPackagesTimestamp.isAfter(now)) {
218
          globals.printError('Maybe the file was concurrently modified?');
219 220
        }
      }
221
    }
222
  }
223

224

225 226 227
  @override
  Future<void> batch(
    List<String> arguments, {
228 229 230 231 232 233 234
    @required PubContext context,
    String directory,
    MessageFilter filter,
    String failureMessage = 'pub failed',
    @required bool retry,
    bool showTraceForErrors,
  }) async {
235
    showTraceForErrors ??= await globals.isRunningOnBot;
236

237
    String lastPubMessage = 'no message';
238 239
    bool versionSolvingFailed = false;
    String filterWrapper(String line) {
240
      lastPubMessage = line;
241 242 243 244 245 246 247
      if (line.contains('version solving failed')) {
        versionSolvingFailed = true;
      }
      if (filter == null) {
        return line;
      }
      return filter(line);
248
    }
249 250 251

    if (showTraceForErrors) {
      arguments.insert(0, '--trace');
252
    }
253 254 255
    int attempts = 0;
    int duration = 1;
    int code;
256
    loop: while (true) {
257 258 259 260
      attempts += 1;
      code = await processUtils.stream(
        _pubCommand(arguments),
        workingDirectory: directory,
261
        mapFunction: filterWrapper, // may set versionSolvingFailed, lastPubMessage
262
        environment: await _createPubEnvironment(context),
263
      );
264 265 266 267 268 269 270
      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;
271
      }
272
      assert(message != null);
273
      versionSolvingFailed = false;
274
      globals.printStatus('$failureMessage ($message) -- attempting retry $attempts in $duration second${ duration == 1 ? "" : "s"}...');
275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291
      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,
    ).send();
292

293
    if (code != 0) {
294
      throwToolExit('$failureMessage ($code; $lastPubMessage)', exitCode: code);
295
    }
296
  }
297 298 299 300

  @override
  Future<void> interactively(
    List<String> arguments, {
301
    String directory,
302 303 304
  }) async {
    Cache.releaseLockEarly();
    final io.Process process = await processUtils.start(
305
      _pubCommand(arguments),
306
      workingDirectory: directory,
307
      environment: await _createPubEnvironment(PubContext.interactive),
308
    );
309

310
    // Pipe the Flutter tool stdin to the pub stdin.
311
    unawaited(process.stdin.addStream(globals.stdio.stdin)
312 313 314 315 316 317 318 319 320 321 322
      // 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<dynamic>(<Future<dynamic>>[
323 324
        globals.stdio.addStdoutStream(process.stdout),
        globals.stdio.addStderrStream(process.stderr),
325
      ]);
326
    } on Exception catch (err, stack) {
327 328 329
      globals.printTrace('Echoing stdout or stderr from the pub subprocess failed:');
      globals.printTrace('$err\n$stack');
    }
330 331 332 333 334 335

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

338 339 340
  /// The command used for running pub.
  List<String> _pubCommand(List<String> arguments) {
    return <String>[sdkBinaryName('pub'), ...arguments];
341
  }
342 343 344

}

345 346
typedef MessageFilter = String Function(String message);

347
/// The full environment used when running pub.
348 349
///
/// [context] provides extra information to package server requests to
350
/// understand usage.
351
Future<Map<String, String>> _createPubEnvironment(PubContext context) async {
352 353
  final Map<String, String> environment = <String, String>{
    'FLUTTER_ROOT': Cache.flutterRoot,
354
    _pubEnvironmentKey: await _getPubEnvironmentValue(context),
355 356 357 358 359 360 361
  };
  final String pubCache = _getRootPubCacheIfAvailable();
  if (pubCache != null) {
    environment[_pubCacheEnvironmentKey] = pubCache;
  }
  return environment;
}
362

363
final RegExp _analyzerWarning = RegExp(r'^! \w+ [^ ]+ from path \.\./\.\./bin/cache/dart-sdk/lib/\w+$');
364

365 366 367
/// The console environment key used by the pub tool.
const String _pubEnvironmentKey = 'PUB_ENVIRONMENT';

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

371 372 373
/// Returns the environment value that should be used when running pub.
///
/// Includes any existing environment variable, if one exists.
374 375
///
/// [context] provides extra information to package server requests to
376
/// understand usage.
377
Future<String> _getPubEnvironmentValue(PubContext pubContext) async {
378 379
  // DO NOT update this function without contacting kevmoo.
  // We have server-side tooling that assumes the values are consistent.
380
  final String existing = globals.platform.environment[_pubEnvironmentKey];
381 382
  final List<String> values = <String>[
    if (existing != null && existing.isNotEmpty) existing,
383
    if (await globals.isRunningOnBot) 'flutter_bot',
384 385 386
    'flutter_cli',
    ...pubContext._values,
  ];
387 388 389
  return values.join(':');
}

390
String _getRootPubCacheIfAvailable() {
391 392
  if (globals.platform.environment.containsKey(_pubCacheEnvironmentKey)) {
    return globals.platform.environment[_pubCacheEnvironmentKey];
393 394
  }

395 396 397
  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.');
398 399 400 401 402 403 404
    return cachePath;
  }

  // Use pub's default location by returning null.
  return null;
}

405
String _filterOverrideWarnings(String message) {
406
  // This function filters out these three messages:
407 408
  //   Warning: You are using these overridden dependencies:
  //   ! analyzer 0.29.0-alpha.0 from path ../../bin/cache/dart-sdk/lib/analyzer
409
  //   ! front_end 0.1.0-alpha.0 from path ../../bin/cache/dart-sdk/lib/front_end
410
  if (message == 'Warning: You are using these overridden dependencies:') {
411
    return null;
412 413
  }
  if (message.contains(_analyzerWarning)) {
414
    return null;
415
  }
416
  return message;
417
}