cache.dart 40.9 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 7
import 'dart:async';

import 'package:crypto/crypto.dart';
8
import 'package:file/memory.dart';
9
import 'package:meta/meta.dart';
10
import 'package:process/process.dart';
11

12
import 'base/common.dart';
13
import 'base/error_handling_io.dart';
14
import 'base/file_system.dart';
15
import 'base/io.dart' show HttpClient, HttpClientRequest, HttpClientResponse, HttpHeaders, HttpStatus, SocketException;
16
import 'base/logger.dart';
17
import 'base/net.dart';
18
import 'base/os.dart' show OperatingSystemUtils;
19
import 'base/platform.dart';
20
import 'base/terminal.dart';
21
import 'base/user_messages.dart';
22
import 'build_info.dart';
23
import 'convert.dart';
24
import 'features.dart';
25 26 27 28 29 30

const String kFlutterRootEnvironmentVariableName = 'FLUTTER_ROOT'; // should point to //flutter/ (root of flutter/flutter repo)
const String kFlutterEngineEnvironmentVariableName = 'FLUTTER_ENGINE'; // should point to //engine/src/ (root of flutter/engine repo)
const String kSnapshotFileName = 'flutter_tools.snapshot'; // in //flutter/bin/cache/
const String kFlutterToolsScriptFileName = 'flutter_tools.dart'; // in //flutter/packages/flutter_tools/bin/
const String kFlutterEnginePackageName = 'sky_engine';
31

32
/// A tag for a set of development artifacts that need to be cached.
33 34
class DevelopmentArtifact {

35
  const DevelopmentArtifact._(this.name, {this.feature});
36 37 38

  /// The name of the artifact.
  ///
39
  /// This should match the flag name in precache.dart.
40 41
  final String name;

42
  /// A feature to control the visibility of this artifact.
43
  final Feature? feature;
44

45
  /// Artifacts required for Android development.
46 47
  static const DevelopmentArtifact androidGenSnapshot = DevelopmentArtifact._('android_gen_snapshot', feature: flutterAndroidFeature);
  static const DevelopmentArtifact androidMaven = DevelopmentArtifact._('android_maven', feature: flutterAndroidFeature);
48

49
  // Artifacts used for internal builds.
50
  static const DevelopmentArtifact androidInternalBuild = DevelopmentArtifact._('android_internal_build', feature: flutterAndroidFeature);
51 52

  /// Artifacts required for iOS development.
53
  static const DevelopmentArtifact iOS = DevelopmentArtifact._('ios', feature: flutterIOSFeature);
54

55
  /// Artifacts required for web development.
56
  static const DevelopmentArtifact web = DevelopmentArtifact._('web', feature: flutterWebFeature);
57 58

  /// Artifacts required for desktop macOS.
59
  static const DevelopmentArtifact macOS = DevelopmentArtifact._('macos', feature: flutterMacOSDesktopFeature);
60 61

  /// Artifacts required for desktop Windows.
62
  static const DevelopmentArtifact windows = DevelopmentArtifact._('windows', feature: flutterWindowsDesktopFeature);
63

64
  /// Artifacts required for desktop Linux.
65
  static const DevelopmentArtifact linux = DevelopmentArtifact._('linux', feature: flutterLinuxDesktopFeature);
66 67

  /// Artifacts required for Fuchsia.
68
  static const DevelopmentArtifact fuchsia = DevelopmentArtifact._('fuchsia', feature: flutterFuchsiaFeature);
69

70
  /// Artifacts required for the Flutter Runner.
71
  static const DevelopmentArtifact flutterRunner = DevelopmentArtifact._('flutter_runner', feature: flutterFuchsiaFeature);
72

73 74 75
  /// Artifacts required for desktop Windows UWP.
  static const DevelopmentArtifact windowsUwp = DevelopmentArtifact._('winuwp', feature: windowsUwpEmbedding);

76
  /// Artifacts required for any development platform.
77 78 79
  ///
  /// This does not need to be explicitly returned from requiredArtifacts as
  /// it will always be downloaded.
80 81
  static const DevelopmentArtifact universal = DevelopmentArtifact._('universal');

82
  /// The values of DevelopmentArtifacts.
83
  static final List<DevelopmentArtifact> values = <DevelopmentArtifact>[
84 85 86
    androidGenSnapshot,
    androidMaven,
    androidInternalBuild,
87 88 89 90 91 92 93
    iOS,
    web,
    macOS,
    windows,
    linux,
    fuchsia,
    universal,
94
    flutterRunner,
95
    windowsUwp,
96
  ];
97 98

  @override
99
  String toString() => 'Artifact($name)';
100 101
}

102
/// A wrapper around the `bin/cache/` directory.
103 104 105
///
/// This does not provide any artifacts by default. See [FlutterCache] for the default
/// artifact set.
106
class Cache {
107
  /// [rootOverride] is configurable for testing.
108
  /// [artifacts] is configurable for testing.
109
  Cache({
110 111 112 113 114 115
    @protected Directory? rootOverride,
    @protected List<ArtifactSet>? artifacts,
    required Logger logger,
    required FileSystem fileSystem,
    required Platform platform,
    required OperatingSystemUtils osUtils,
116
  }) : _rootOverride = rootOverride,
117 118 119 120 121
       _logger = logger,
       _fileSystem = fileSystem,
       _platform = platform,
       _osUtils = osUtils,
      _net = Net(logger: logger, platform: platform),
122
      _fsUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform),
123
      _artifacts = artifacts ?? <ArtifactSet>[];
124

125 126 127 128 129 130
  /// Create a [Cache] for testing.
  ///
  /// Defaults to a memory file system, fake platform,
  /// buffer logger, and no accessible artifacts.
  /// By default, the root cache directory path is "cache".
  factory Cache.test({
131 132 133 134 135 136
    Directory? rootOverride,
    List<ArtifactSet>? artifacts,
    Logger? logger,
    FileSystem? fileSystem,
    Platform? platform,
    required ProcessManager processManager,
137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
  }) {
    fileSystem ??= rootOverride?.fileSystem ?? MemoryFileSystem.test();
    platform ??= FakePlatform(environment: <String, String>{});
    logger ??= BufferLogger.test();
    return Cache(
      rootOverride: rootOverride ??= fileSystem.directory('cache'),
      artifacts: artifacts ?? <ArtifactSet>[],
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
      osUtils: OperatingSystemUtils(
        fileSystem: fileSystem,
        logger: logger,
        platform: platform,
        processManager: processManager,
      ),
    );
  }

156 157 158 159
  final Logger _logger;
  final Platform _platform;
  final FileSystem _fileSystem;
  final OperatingSystemUtils _osUtils;
160
  final Directory? _rootOverride;
161 162 163
  final List<ArtifactSet> _artifacts;
  final Net _net;
  final FileSystemUtils _fsUtils;
164

165
  late final ArtifactUpdater _artifactUpdater = _createUpdater();
166

167
  @visibleForTesting
168 169 170 171 172
  @protected
  void registerArtifact(ArtifactSet artifactSet) {
    _artifacts.add(artifactSet);
  }

173 174 175 176 177 178 179
  /// This has to be lazy because it requires FLUTTER_ROOT to be initialized.
  ArtifactUpdater _createUpdater() {
    return ArtifactUpdater(
      operatingSystemUtils: _osUtils,
      logger: _logger,
      fileSystem: _fileSystem,
      tempStorage: getDownloadDir(),
180 181
      platform: _platform,
      httpClient: HttpClient(),
182 183 184
    );
  }

185
  static const List<String> _hostsBlockedInChina = <String> [
186 187 188
    'storage.googleapis.com',
  ];

189
  // Initialized by FlutterCommandRunner on startup.
190 191
  // Explore making this field lazy to catch non-initialized access.
  static String? flutterRoot;
192

193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213
  /// Determine the absolute and normalized path for the root of the current
  /// Flutter checkout.
  ///
  /// This method has a series of fallbacks for determining the repo location. The
  /// first success will immediately return the root without further checks.
  ///
  /// The order of these tests is:
  ///   1. FLUTTER_ROOT environment variable contains the path.
  ///   2. Platform script is a data URI scheme, returning `../..` to support
  ///      tests run from `packages/flutter_tools`.
  ///   3. Platform script is package URI scheme, returning the grandparent directory
  ///      of the package config file location from `packages/flutter_tools/.packages`.
  ///   4. Platform script file path is the snapshot path generated by `bin/flutter`,
  ///      returning the grandparent directory from `bin/cache`.
  ///   5. Platform script file name is the entrypoint in `packages/flutter_tools/bin/flutter_tools.dart`,
  ///      returning the 4th parent directory.
  ///   6. The current directory
  ///
  /// If an exception is thrown during any of these checks, an error message is
  /// printed and `.` is returned by default (6).
  static String defaultFlutterRoot({
214 215 216
    required Platform platform,
    required FileSystem fileSystem,
    required UserMessages userMessages,
217 218 219 220 221
  }) {
    String normalize(String path) {
      return fileSystem.path.normalize(fileSystem.path.absolute(path));
    }
    if (platform.environment.containsKey(kFlutterRootEnvironmentVariableName)) {
222
      return normalize(platform.environment[kFlutterRootEnvironmentVariableName]!);
223 224 225 226 227 228 229 230
    }
    try {
      if (platform.script.scheme == 'data') {
        return normalize('../..'); // The tool is running as a test.
      }
      final String Function(String) dirname = fileSystem.path.dirname;

      if (platform.script.scheme == 'package') {
231
        final String packageConfigPath = Uri.parse(platform.packageConfig!).toFilePath(
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249
          windows: platform.isWindows,
        );
        return normalize(dirname(dirname(dirname(packageConfigPath))));
      }

      if (platform.script.scheme == 'file') {
        final String script = platform.script.toFilePath(
          windows: platform.isWindows,
        );
        if (fileSystem.path.basename(script) == kSnapshotFileName) {
          return normalize(dirname(dirname(fileSystem.path.dirname(script))));
        }
        if (fileSystem.path.basename(script) == kFlutterToolsScriptFileName) {
          return normalize(dirname(dirname(dirname(dirname(script)))));
        }
      }
    } on Exception catch (error) {
      // There is currently no logger attached since this is computed at startup.
250
      // ignore: avoid_print
251 252 253 254 255
      print(userMessages.runnerNoRoot('$error'));
    }
    return normalize('.');
  }

256 257 258 259
  // Whether to cache artifacts for all platforms. Defaults to only caching
  // artifacts for the current platform.
  bool includeAllPlatforms = false;

260 261
  // Names of artifacts which should be cached even if they would normally
  // be filtered out for the current platform.
262
  Set<String>? platformOverrideArtifacts;
263

264 265 266
  // Whether to cache the unsigned mac binaries. Defaults to caching the signed binaries.
  bool useUnsignedMacBinaries = false;

267
  static RandomAccessFile? _lock;
268 269
  static bool _lockEnabled = true;

270
  /// Turn off the [lock]/[releaseLock] mechanism.
271 272 273
  ///
  /// This is used by the tests since they run simultaneously and all in one
  /// process and so it would be a mess if they had to use the lock.
274
  @visibleForTesting
275 276 277 278
  static void disableLocking() {
    _lockEnabled = false;
  }

279
  /// Turn on the [lock]/[releaseLock] mechanism.
280 281 282 283 284 285 286
  ///
  /// This is used by the tests.
  @visibleForTesting
  static void enableLocking() {
    _lockEnabled = true;
  }

287 288 289 290 291 292 293 294
  /// Check if lock acquired, skipping FLUTTER_ALREADY_LOCKED reentrant checks.
  ///
  /// This is used by the tests.
  @visibleForTesting
  static bool isLocked() {
    return _lock != null;
  }

295 296
  /// Lock the cache directory.
  ///
297 298
  /// This happens while required artifacts are updated
  /// (see [FlutterCommandRunner.runCommand]).
299
  ///
300
  /// This uses normal POSIX flock semantics.
301
  Future<void> lock() async {
302
    if (!_lockEnabled) {
303
      return;
304
    }
305
    assert(_lock == null);
306
    final File lockFile =
307
      _fileSystem.file(_fileSystem.path.join(flutterRoot!, 'bin', 'cache', 'lockfile'));
308 309 310
    try {
      _lock = lockFile.openSync(mode: FileMode.write);
    } on FileSystemException catch (e) {
311 312
      _logger.printError('Failed to open or create the artifact cache lockfile: "$e"');
      _logger.printError('Please ensure you have permissions to create or open ${lockFile.path}');
313 314
      throwToolExit('Failed to open or create the lockfile');
    }
315 316 317 318
    bool locked = false;
    bool printed = false;
    while (!locked) {
      try {
319
        _lock!.lockSync();
320 321 322
        locked = true;
      } on FileSystemException {
        if (!printed) {
323
          _logger.printTrace('Waiting to be able to obtain lock of Flutter binary artifacts directory: ${_lock!.path}');
324 325 326 327 328 329 330
          // This needs to go to stderr to avoid cluttering up stdout if a
          // parent process is collecting stdout (e.g. when calling "flutter
          // version --machine"). It's not really a "warning" though, so print it
          // in grey. Also, make sure that it isn't counted as a warning for
          // Logger.warningsAreFatal.
          final bool oldWarnings = _logger.hadWarningOutput;
          _logger.printWarning(
331 332 333
            'Waiting for another flutter command to release the startup lock...',
            color: TerminalColor.grey,
          );
334
          _logger.hadWarningOutput = oldWarnings;
335 336
          printed = true;
        }
337
        await Future<void>.delayed(const Duration(milliseconds: 50));
338 339 340 341
      }
    }
  }

342 343 344 345
  /// Releases the lock.
  ///
  /// This happens automatically on startup (see [FlutterCommand.verifyThenRunCommand])
  /// after the command's required artifacts are updated.
346
  void releaseLock() {
347
    if (!_lockEnabled || _lock == null) {
348
      return;
349
    }
350
    _lock!.closeSync();
351 352 353
    _lock = null;
  }

354 355
  /// Checks if the current process owns the lock for the cache directory at
  /// this very moment; throws a [StateError] if it doesn't.
356 357
  void checkLockAcquired() {
    if (_lockEnabled && _lock == null && _platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') {
358
      throw StateError(
359 360 361 362 363
        'The current process does not own the lock for the cache directory. This is a bug in Flutter CLI tools.',
      );
    }
  }

364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392
  String get devToolsVersion {
    if (_devToolsVersion == null) {
      const String devToolsDirPath = 'dart-sdk/bin/resources/devtools';
      final Directory devToolsDir = getCacheDir(devToolsDirPath, shouldCreate: false);
      if (!devToolsDir.existsSync()) {
        throw Exception('Could not find directory at ${devToolsDir.path}');
      }
      final String versionFilePath = '${devToolsDir.path}/version.json';
      final File versionFile = _fileSystem.file(versionFilePath);
      if (!versionFile.existsSync()) {
        throw Exception('Could not find file at $versionFilePath');
      }
      final dynamic data = jsonDecode(versionFile.readAsStringSync());
      if (data is! Map<String, Object>) {
        throw Exception("Expected object of type 'Map<String, Object>' but got one of type '${data.runtimeType}'");
      }
      final dynamic version = data['version'];
      if (version == null) {
        throw Exception('Could not parse DevTools version from $version');
      }
      if (version is! String) {
        throw Exception("Could not parse DevTools version. Expected object of type 'String', but got one of type '${version.runtimeType}'");
      }
      return _devToolsVersion = version;
    }
    return _devToolsVersion!;
  }
  String ? _devToolsVersion;

393
  /// The current version of Dart used to build Flutter and run the tool.
394 395 396 397
  String get dartSdkVersion {
    if (_dartSdkVersion == null) {
      // Make the version string more customer-friendly.
      // Changes '2.1.0-dev.8.0.flutter-4312ae32' to '2.1.0 (build 2.1.0-dev.8.0 4312ae32)'
398
      final String justVersion = _platform.version.split(' ')[0];
399
      _dartSdkVersion = justVersion.replaceFirstMapped(RegExp(r'(\d+\.\d+\.\d+)(.+)'), (Match match) {
400
        final String noFlutter = match[2]!.replaceAll('.flutter-', ' ');
401 402 403
        return '${match[1]} (build ${match[1]}$noFlutter)';
      });
    }
404
    return _dartSdkVersion!;
405
  }
406
  String? _dartSdkVersion;
407

408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423
  /// The current version of Dart used to build Flutter and run the tool.
  String get dartSdkBuild {
    if (_dartSdkBuild == null) {
      // Make the version string more customer-friendly.
      // Changes '2.1.0-dev.8.0.flutter-4312ae32' to '2.1.0 (build 2.1.0-dev.8.0 4312ae32)'
      final String justVersion = _platform.version.split(' ')[0];
      _dartSdkBuild = justVersion.replaceFirstMapped(RegExp(r'(\d+\.\d+\.\d+)(.+)'), (Match match) {
        final String noFlutter = match[2]!.replaceAll('.flutter-', ' ');
        return '${match[1]}$noFlutter';
      });
    }
    return _dartSdkBuild!;
  }
  String? _dartSdkBuild;


424
  /// The current version of the Flutter engine the flutter tool will download.
425 426
  String get engineRevision {
    _engineRevision ??= getVersionFor('engine');
427 428 429 430
    if (_engineRevision == null) {
      throwToolExit('Could not determine engine revision.');
    }
    return _engineRevision!;
431
  }
432
  String? _engineRevision;
433

434
  String get storageBaseUrl {
435
    final String? overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
    if (overrideUrl == null) {
      return 'https://storage.googleapis.com';
    }
    // verify that this is a valid URI.
    try {
      Uri.parse(overrideUrl);
    } on FormatException catch (err) {
      throwToolExit('"FLUTTER_STORAGE_BASE_URL" contains an invalid URI:\n$err');
    }
    _maybeWarnAboutStorageOverride(overrideUrl);
    return overrideUrl;
  }

  bool _hasWarnedAboutStorageOverride = false;

  void _maybeWarnAboutStorageOverride(String overrideUrl) {
    if (_hasWarnedAboutStorageOverride) {
      return;
    }
455
    _logger.printStatus(
456 457 458 459 460 461
      'Flutter assets will be downloaded from $overrideUrl. Make sure you trust this source!',
      emphasis: true,
    );
    _hasWarnedAboutStorageOverride = true;
  }

462
  /// Return the top-level directory in the cache; this is `bin/cache`.
463
  Directory getRoot() {
464
    if (_rootOverride != null) {
465
      return _fileSystem.directory(_fileSystem.path.join(_rootOverride!.path, 'bin', 'cache'));
466
    } else {
467
      return _fileSystem.directory(_fileSystem.path.join(flutterRoot!, 'bin', 'cache'));
468
    }
469
  }
470

471 472 473 474
  String getHostPlatformArchName() {
    return getNameForHostPlatformArch(_osUtils.hostPlatform);
  }

475
  /// Return a directory in the cache dir. For `pkg`, this will return `bin/cache/pkg`.
476 477 478 479
  ///
  /// When [shouldCreate] is true, the cache directory at [name] will be created
  /// if it does not already exist.
  Directory getCacheDir(String name, { bool shouldCreate = true }) {
480
    final Directory dir = _fileSystem.directory(_fileSystem.path.join(getRoot().path, name));
481
    if (!dir.existsSync() && shouldCreate) {
482
      dir.createSync(recursive: true);
483
      _osUtils.chmod(dir, '755');
484
    }
485
    return dir;
486 487
  }

488 489 490
  /// Return the top-level directory for artifact downloads.
  Directory getDownloadDir() => getCacheDir('downloads');

491 492 493
  /// Return the top-level mutable directory in the cache; this is `bin/cache/artifacts`.
  Directory getCacheArtifacts() => getCacheDir('artifacts');

494
  /// Location of LICENSE file.
495
  File getLicenseFile() => _fileSystem.file(_fileSystem.path.join(flutterRoot!, 'LICENSE'));
496

497 498 499
  /// Get a named directory from with the cache's artifact directory; for example,
  /// `material_fonts` would return `bin/cache/artifacts/material_fonts`.
  Directory getArtifactDirectory(String name) {
500
    return getCacheArtifacts().childDirectory(name);
501 502
  }

503 504
  MapEntry<String, String> get dyLdLibEntry {
    if (_dyLdLibEntry != null) {
505
      return _dyLdLibEntry!;
506 507
    }
    final List<String> paths = <String>[];
508
    for (final ArtifactSet artifact in _artifacts) {
509 510 511 512
      final Map<String, String> env = artifact.environment;
      if (env == null || !env.containsKey('DYLD_LIBRARY_PATH')) {
        continue;
      }
513
      final String path = env['DYLD_LIBRARY_PATH']!;
514 515
      if (path.isEmpty) {
        continue;
516
      }
517
      paths.add(path);
518 519
    }
    _dyLdLibEntry = MapEntry<String, String>('DYLD_LIBRARY_PATH', paths.join(':'));
520
    return _dyLdLibEntry!;
521
  }
522
  MapEntry<String, String>? _dyLdLibEntry;
523

524 525 526 527 528 529
  /// The web sdk has to be co-located with the dart-sdk so that they can share source
  /// code.
  Directory getWebSdkDirectory() {
    return getRoot().childDirectory('flutter_web_sdk');
  }

530
  String? getVersionFor(String artifactName) {
531
    final File versionFile = _fileSystem.file(_fileSystem.path.join(
532
      _rootOverride?.path ?? flutterRoot!,
533 534 535 536
      'bin',
      'internal',
      '$artifactName.version',
    ));
537 538 539
    return versionFile.existsSync() ? versionFile.readAsStringSync().trim() : null;
  }

540
  /// Delete all stamp files maintained by the cache.
541 542 543 544 545
  void clearStampFiles() {
    try {
      getStampFileFor('flutter_tools').deleteSync();
      for (final ArtifactSet artifact in _artifacts) {
        final File file = getStampFileFor(artifact.stampName);
546
        ErrorHandlingFileSystem.deleteIfExists(file);
547 548
      }
    } on FileSystemException catch (err) {
549
      _logger.printWarning('Failed to delete some stamp files: $err');
550 551 552
    }
  }

553 554 555
  /// Read the stamp for [artifactName].
  ///
  /// If the file is missing or cannot be parsed, returns `null`.
556
  String? getStampFor(String artifactName) {
557
    final File stampFile = getStampFileFor(artifactName);
558 559 560 561 562 563 564 565
    if (!stampFile.existsSync()) {
      return null;
    }
    try {
      return stampFile.readAsStringSync().trim();
    } on FileSystemException {
      return null;
    }
566 567
  }

568 569 570 571 572
  void setStampFor(String artifactName, String version) {
    getStampFileFor(artifactName).writeAsStringSync(version);
  }

  File getStampFileFor(String artifactName) {
573
    return _fileSystem.file(_fileSystem.path.join(getRoot().path, '$artifactName.stamp'));
574 575
  }

576 577 578
  /// Returns `true` if either [entity] is older than the tools stamp or if
  /// [entity] doesn't exist.
  bool isOlderThanToolsStamp(FileSystemEntity entity) {
579
    final File flutterToolsStamp = getStampFileFor('flutter_tools');
580
    return _fsUtils.isOlderThanReference(
581 582 583
      entity: entity,
      referenceFile: flutterToolsStamp,
    );
584 585
  }

586 587
  Future<bool> isUpToDate() async {
    for (final ArtifactSet artifact in _artifacts) {
588
      if (!await artifact.isUpToDate(_fileSystem)) {
589 590 591 592 593
        return false;
      }
    }
    return true;
  }
594

595 596 597
  /// Update the cache to contain all `requiredArtifacts`.
  Future<void> updateAll(Set<DevelopmentArtifact> requiredArtifacts) async {
    if (!_lockEnabled) {
598
      return;
599
    }
600
    for (final ArtifactSet artifact in _artifacts) {
601
      if (!requiredArtifacts.contains(artifact.developmentArtifact)) {
602
        _logger.printTrace('Artifact $artifact is not required, skipping update.');
603
        continue;
604
      }
605
      if (await artifact.isUpToDate(_fileSystem)) {
606 607 608
        continue;
      }
      try {
609
        await artifact.update(_artifactUpdater, _logger, _fileSystem, _osUtils);
610 611
      } on SocketException catch (e) {
        if (_hostsBlockedInChina.contains(e.address?.host)) {
612
          _logger.printError(
613
            'Failed to retrieve Flutter tool dependencies: ${e.message}.\n'
614
            "If you're in China, please see this page: "
615 616 617 618 619
            'https://flutter.dev/community/china',
            emphasis: true,
          );
        }
        rethrow;
620
      }
621 622
    }
  }
623 624

  Future<bool> areRemoteArtifactsAvailable({
625
    String? engineVersion,
626 627
    bool includeAllPlatforms = true,
  }) async {
628
    final bool includeAllPlatformsState = this.includeAllPlatforms;
629
    bool allAvailable = true;
630
    this.includeAllPlatforms = includeAllPlatforms;
631
    for (final ArtifactSet cachedArtifact in _artifacts) {
632
      if (cachedArtifact is EngineCachedArtifact) {
633
        allAvailable &= await cachedArtifact.checkForArtifacts(engineVersion);
634 635
      }
    }
636
    this.includeAllPlatforms = includeAllPlatformsState;
637
    return allAvailable;
638
  }
639 640

  Future<bool> doesRemoteExist(String message, Uri url) async {
641
    final Status status = _logger.startProgress(
642 643 644 645 646 647 648 649 650 651
      message,
    );
    bool exists;
    try {
      exists = await _net.doesRemoteFileExist(url);
    } finally {
      status.stop();
    }
    return exists;
  }
652 653
}

654 655 656 657 658 659 660 661
/// Representation of a set of artifacts used by the tool.
abstract class ArtifactSet {
  ArtifactSet(this.developmentArtifact) : assert(developmentArtifact != null);

  /// The development artifact.
  final DevelopmentArtifact developmentArtifact;

  /// [true] if the artifact is up to date.
662
  Future<bool> isUpToDate(FileSystem fileSystem);
663 664 665 666 667 668 669

  /// The environment variables (if any) required to consume the artifacts.
  Map<String, String> get environment {
    return const <String, String>{};
  }

  /// Updates the artifact.
670 671 672 673 674 675
  Future<void> update(
    ArtifactUpdater artifactUpdater,
    Logger logger,
    FileSystem fileSystem,
    OperatingSystemUtils operatingSystemUtils,
  );
676 677 678 679 680 681 682

  /// The canonical name of the artifact.
  String get name;

  // The name of the stamp file. Defaults to the same as the
  // artifact name.
  String get stampName => name;
683 684 685 686 687 688 689 690 691
}

/// An artifact set managed by the cache.
abstract class CachedArtifact extends ArtifactSet {
  CachedArtifact(
    this.name,
    this.cache,
    DevelopmentArtifact developmentArtifact,
  ) : super(developmentArtifact);
692

693
  final Cache cache;
694

695
  @override
696 697
  final String name;

698
  @override
699 700
  String get stampName => name;

701
  Directory get location => cache.getArtifactDirectory(name);
702 703

  String? get version => cache.getVersionFor(name);
704

705 706 707
  // Whether or not to bypass normal platform filtering for this artifact.
  bool get ignorePlatformFiltering {
    return cache.includeAllPlatforms ||
708
      (cache.platformOverrideArtifacts != null && cache.platformOverrideArtifacts!.contains(developmentArtifact.name));
709 710
  }

711
  @override
712
  Future<bool> isUpToDate(FileSystem fileSystem) async {
713
    if (!location.existsSync()) {
714
      return false;
715 716
    }
    if (version != cache.getStampFor(stampName)) {
717
      return false;
718
    }
719
    return isUpToDateInner(fileSystem);
720 721
  }

722
  @override
723 724 725 726 727 728
  Future<void> update(
    ArtifactUpdater artifactUpdater,
    Logger logger,
    FileSystem fileSystem,
    OperatingSystemUtils operatingSystemUtils,
  ) async {
729
    if (!location.existsSync()) {
730 731 732
      try {
        location.createSync(recursive: true);
      } on FileSystemException catch (err) {
733
        logger.printError(err.toString());
734 735 736 737 738
        throwToolExit(
          'Failed to create directory for flutter cache at ${location.path}. '
          'Flutter may be missing permissions in its cache directory.'
        );
      }
739
    }
740
    await updateInner(artifactUpdater, fileSystem, operatingSystemUtils);
741
    try {
742
      if (version == null) {
743
        logger.printWarning(
744 745 746 747 748 749 750
          'No known version for the artifact name "$name". '
          'Flutter can continue, but the artifact may be re-downloaded on '
          'subsequent invocations until the problem is resolved.',
        );
      } else {
        cache.setStampFor(stampName, version!);
      }
751
    } on FileSystemException catch (err) {
752
      logger.printWarning(
753 754 755 756 757 758
        'The new artifact "$name" was downloaded, but Flutter failed to update '
        'its stamp file, receiving the error "$err". '
        'Flutter can continue, but the artifact may be re-downloaded on '
        'subsequent invocations until the problem is resolved.',
      );
    }
759
    artifactUpdater.removeDownloadedFiles();
760 761
  }

762
  /// Hook method for extra checks for being up-to-date.
763
  bool isUpToDateInner(FileSystem fileSystem) => true;
764

765 766 767 768 769
  Future<void> updateInner(
    ArtifactUpdater artifactUpdater,
    FileSystem fileSystem,
    OperatingSystemUtils operatingSystemUtils,
  );
770
}
771

772

773 774 775 776
abstract class EngineCachedArtifact extends CachedArtifact {
  EngineCachedArtifact(
    this.stampName,
    Cache cache,
777 778
    DevelopmentArtifact developmentArtifact,
  ) : super('engine', cache, developmentArtifact);
779

780 781
  @override
  final String stampName;
782

783 784
  /// Return a list of (directory path, download URL path) tuples.
  List<List<String>> getBinaryDirs();
785

786 787
  /// A list of cache directory paths to which the LICENSE file should be copied.
  List<String> getLicenseDirs();
788

789 790
  /// A list of the dart package directories to download.
  List<String> getPackageDirs();
791

792
  @override
793
  bool isUpToDateInner(FileSystem fileSystem) {
794
    final Directory pkgDir = cache.getCacheDir('pkg');
795
    for (final String pkgName in getPackageDirs()) {
796 797
      final String pkgPath = fileSystem.path.join(pkgDir.path, pkgName);
      if (!fileSystem.directory(pkgPath).existsSync()) {
798
        return false;
799
      }
800
    }
801

802
    for (final List<String> toolsDir in getBinaryDirs()) {
803
      final Directory dir = fileSystem.directory(fileSystem.path.join(location.path, toolsDir[0]));
804
      if (!dir.existsSync()) {
805
        return false;
806
      }
807
    }
808

809
    for (final String licenseDir in getLicenseDirs()) {
810
      final File file = fileSystem.file(fileSystem.path.join(location.path, licenseDir, 'LICENSE'));
811
      if (!file.existsSync()) {
812
        return false;
813
      }
814 815
    }
    return true;
816 817
  }

818
  @override
819 820 821 822 823
  Future<void> updateInner(
    ArtifactUpdater artifactUpdater,
    FileSystem fileSystem,
    OperatingSystemUtils operatingSystemUtils,
  ) async {
824
    final String url = '${cache.storageBaseUrl}/flutter_infra_release/flutter/$version/';
825 826

    final Directory pkgDir = cache.getCacheDir('pkg');
827
    for (final String pkgName in getPackageDirs()) {
828
      await artifactUpdater.downloadZipArchive('Downloading package $pkgName...', Uri.parse('$url$pkgName.zip'), pkgDir);
829 830
    }

831
    for (final List<String> toolsDir in getBinaryDirs()) {
832 833
      final String cacheDir = toolsDir[0];
      final String urlPath = toolsDir[1];
834
      final Directory dir = fileSystem.directory(fileSystem.path.join(location.path, cacheDir));
835 836 837

      // Avoid printing things like 'Downloading linux-x64 tools...' multiple times.
      final String friendlyName = urlPath.replaceAll('/artifacts.zip', '').replaceAll('.zip', '');
838
      await artifactUpdater.downloadZipArchive('Downloading $friendlyName tools...', Uri.parse(url + urlPath), dir);
839

840
      _makeFilesExecutable(dir, operatingSystemUtils);
841

842 843 844
      final File frameworkZip = fileSystem.file(fileSystem.path.join(dir.path, 'FlutterMacOS.framework.zip'));
      if (frameworkZip.existsSync()) {
        final Directory framework = fileSystem.directory(fileSystem.path.join(dir.path, 'FlutterMacOS.framework'));
845
        ErrorHandlingFileSystem.deleteIfExists(framework, recursive: true);
846 847
        framework.createSync();
        operatingSystemUtils.unzip(frameworkZip, framework);
Devon Carew's avatar
Devon Carew committed
848 849
      }
    }
850

851
    final File licenseSource = cache.getLicenseFile();
852
    for (final String licenseDir in getLicenseDirs()) {
853
      final String licenseDestinationPath = fileSystem.path.join(location.path, licenseDir, 'LICENSE');
854 855
      await licenseSource.copy(licenseDestinationPath);
    }
856 857
  }

858
  Future<bool> checkForArtifacts(String? engineVersion) async {
859
    engineVersion ??= version;
860
    final String url = '${cache.storageBaseUrl}/flutter_infra_release/flutter/$engineVersion/';
861

862
    bool exists = false;
863
    for (final String pkgName in getPackageDirs()) {
864
      exists = await cache.doesRemoteExist('Checking package $pkgName is available...', Uri.parse('$url$pkgName.zip'));
865 866
      if (!exists) {
        return false;
867
      }
868
    }
869

870
    for (final List<String> toolsDir in getBinaryDirs()) {
871 872
      final String cacheDir = toolsDir[0];
      final String urlPath = toolsDir[1];
873
      exists = await cache.doesRemoteExist('Checking $cacheDir tools are available...',
874 875 876
          Uri.parse(url + urlPath));
      if (!exists) {
        return false;
877
      }
878
    }
879
    return true;
880 881
  }

882 883 884 885 886 887 888 889
  void _makeFilesExecutable(Directory dir, OperatingSystemUtils operatingSystemUtils) {
    operatingSystemUtils.chmod(dir, 'a+r,a+x');
    for (final File file in dir.listSync(recursive: true).whereType<File>()) {
      final FileStat stat = file.statSync();
      final bool isUserExecutable = ((stat.mode >> 6) & 0x1) == 1;
      if (file.basename == 'flutter_tester' || isUserExecutable) {
        // Make the file readable and executable by all users.
        operatingSystemUtils.chmod(file, 'a+r,a+x');
890 891 892
      }
    }
  }
893 894
}

895 896 897 898
/// An API for downloading and un-archiving artifacts, such as engine binaries or
/// additional source code.
class ArtifactUpdater {
  ArtifactUpdater({
899 900 901 902 903 904
    required OperatingSystemUtils operatingSystemUtils,
    required Logger logger,
    required FileSystem fileSystem,
    required Directory tempStorage,
    required HttpClient httpClient,
    required Platform platform,
905
  }) : _operatingSystemUtils = operatingSystemUtils,
906
       _httpClient = httpClient,
907 908
       _logger = logger,
       _fileSystem = fileSystem,
909 910 911 912 913
       _tempStorage = tempStorage,
       _platform = platform;

  /// The number of times the artifact updater will repeat the artifact download loop.
  static const int _kRetryCount = 2;
914 915 916 917 918

  final Logger _logger;
  final OperatingSystemUtils _operatingSystemUtils;
  final FileSystem _fileSystem;
  final Directory _tempStorage;
919 920
  final HttpClient _httpClient;
  final Platform _platform;
921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961

  /// Keep track of the files we've downloaded for this execution so we
  /// can delete them after completion. We don't delete them right after
  /// extraction in case [update] is interrupted, so we can restart without
  /// starting from scratch.
  @visibleForTesting
  final List<File> downloadedFiles = <File>[];

  /// Download a zip archive from the given [url] and unzip it to [location].
  Future<void> downloadZipArchive(
    String message,
    Uri url,
    Directory location,
  ) {
    return _downloadArchive(
      message,
      url,
      location,
      _operatingSystemUtils.unzip,
    );
  }

  /// Download a gzipped tarball from the given [url] and unpack it to [location].
  Future<void> downloadZippedTarball(String message, Uri url, Directory location) {
    return _downloadArchive(
      message,
      url,
      location,
      _operatingSystemUtils.unpack,
    );
  }

  /// Download an archive from the given [url] and unzip it to [location].
  Future<void> _downloadArchive(
    String message,
    Uri url,
    Directory location,
    void Function(File, Directory) extractor,
  ) async {
    final String downloadPath = flattenNameSubdirs(url, _fileSystem);
    final File tempFile = _createDownloadFile(downloadPath);
962
    Status status;
963
    int retries = _kRetryCount;
964 965

    while (retries > 0) {
966 967 968
      status = _logger.startProgress(
        message,
      );
969 970
      try {
        _ensureExists(tempFile.parent);
971 972 973 974 975
        if (tempFile.existsSync()) {
          tempFile.deleteSync();
        }
        await _download(url, tempFile);

976 977 978
        if (!tempFile.existsSync()) {
          throw Exception('Did not find downloaded file ${tempFile.path}');
        }
979 980 981 982 983
      } on Exception catch (err) {
        _logger.printTrace(err.toString());
        retries -= 1;
        if (retries == 0) {
          throwToolExit(
984
            'Failed to download $url. Ensure you have network connectivity and then try again.\n$err',
985 986 987 988
          );
        }
        continue;
      } on ArgumentError catch (error) {
989
        final String? overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
990 991 992 993 994 995 996 997 998 999 1000 1001 1002
        if (overrideUrl != null && url.toString().contains(overrideUrl)) {
          _logger.printError(error.toString());
          throwToolExit(
            'The value of FLUTTER_STORAGE_BASE_URL ($overrideUrl) could not be '
            'parsed as a valid url. Please see https://flutter.dev/community/china '
            'for an example of how to use it.\n'
            'Full URL: $url',
            exitCode: kNetworkProblemExitCode,
          );
        }
        // This error should not be hit if there was not a storage URL override, allow the
        // tool to crash.
        rethrow;
1003 1004 1005
      } finally {
        status.stop();
      }
1006 1007 1008 1009 1010
      /// Unzipping multiple file into a directory will not remove old files
      /// from previous versions that are not present in the new bundle.
      final Directory destination = location.childDirectory(
        tempFile.fileSystem.path.basenameWithoutExtension(tempFile.path)
      );
1011 1012 1013 1014 1015 1016 1017 1018 1019 1020
      try {
        ErrorHandlingFileSystem.deleteIfExists(
          destination,
          recursive: true,
        );
      } on FileSystemException catch (error) {
        // Error that indicates another program has this file open and that it
        // cannot be deleted. For the cache, this is either the analyzer reading
        // the sky_engine package or a running flutter_tester device.
        const int kSharingViolation = 32;
1021
        if (_platform.isWindows && error.osError?.errorCode == kSharingViolation) {
1022 1023 1024 1025 1026 1027 1028
          throwToolExit(
            'Failed to delete ${destination.path} because the local file/directory is in use '
            'by another process. Try closing any running IDEs or editors and trying '
            'again'
          );
        }
      }
1029 1030 1031 1032
      _ensureExists(location);

      try {
        extractor(tempFile, location);
1033
      } on Exception catch (err) {
1034 1035
        retries -= 1;
        if (retries == 0) {
1036 1037
          throwToolExit(
            'Flutter could not download and/or extract $url. Ensure you have '
1038
            'network connectivity and all of the required dependencies listed at '
1039 1040
            'flutter.dev/setup.\nThe original exception was: $err.'
          );
1041 1042 1043
        }
        _deleteIgnoringErrors(tempFile);
        continue;
1044 1045 1046 1047 1048
      }
      return;
    }
  }

1049
  /// Download bytes from [url], throwing non-200 responses as an exception.
1050 1051 1052 1053 1054 1055 1056
  ///
  /// Validates that the md5 of the content bytes matches the provided
  /// `x-goog-hash` header, if present. This header should contain an md5 hash
  /// if the download source is Google cloud storage.
  ///
  /// See also:
  ///   * https://cloud.google.com/storage/docs/xml-api/reference-headers#xgooghash
1057
  Future<void> _download(Uri url, File file) async {
1058 1059 1060 1061 1062
    final HttpClientRequest request = await _httpClient.getUrl(url);
    final HttpClientResponse response = await request.close();
    if (response.statusCode != HttpStatus.ok) {
      throw Exception(response.statusCode);
    }
1063

1064 1065 1066
    final String? md5Hash = _expectedMd5(response.headers);
    ByteConversionSink? inputSink;
    late StreamController<Digest> digests;
1067 1068 1069 1070 1071 1072
    if (md5Hash != null) {
      _logger.printTrace('Content $url md5 hash: $md5Hash');
      digests = StreamController<Digest>();
      inputSink = md5.startChunkedConversion(digests);
    }
    final RandomAccessFile randomAccessFile = file.openSync(mode: FileMode.writeOnly);
1073
    await response.forEach((List<int> chunk) {
1074 1075
      inputSink?.add(chunk);
      randomAccessFile.writeFromSync(chunk);
1076
    });
1077 1078 1079 1080 1081 1082
    randomAccessFile.closeSync();
    if (inputSink != null) {
      inputSink.close();
      final Digest digest = await digests.stream.last;
      final String rawDigest = base64.encode(digest.bytes);
      if (rawDigest != md5Hash) {
1083
        throw Exception(
1084 1085 1086 1087 1088 1089 1090 1091 1092
          'Expected $url to have md5 checksum $md5Hash, but was $rawDigest. This '
          'may indicate a problem with your connection to the Flutter backend servers. '
          'Please re-try the download after confirming that your network connection is '
          'stable.'
        );
      }
    }
  }

1093 1094
  String? _expectedMd5(HttpHeaders httpHeaders) {
    final List<String>? values = httpHeaders['x-goog-hash'];
1095 1096 1097
    if (values == null) {
      return null;
    }
1098 1099 1100 1101 1102 1103 1104
    String? rawMd5Hash;
    for (final String value in values) {
      if (value.startsWith('md5=')) {
        rawMd5Hash = value;
        break;
      }
    }
1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116
    if (rawMd5Hash == null) {
      return null;
    }
    final List<String> segments = rawMd5Hash.split('md5=');
    if (segments.length < 2) {
      return null;
    }
    final String md5Hash = segments[1];
    if (md5Hash.isEmpty) {
      return null;
    }
    return md5Hash;
1117 1118
  }

1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133
  /// Create a temporary file and invoke [onTemporaryFile] with the file as
  /// argument, then add the temporary file to the [downloadedFiles].
  File _createDownloadFile(String name) {
    final File tempFile = _fileSystem.file(_fileSystem.path.join(_tempStorage.path, name));
    downloadedFiles.add(tempFile);
    return tempFile;
  }

  /// Create the given [directory] and parents, as necessary.
  void _ensureExists(Directory directory) {
    if (!directory.existsSync()) {
      directory.createSync(recursive: true);
    }
  }

1134
  /// Clear any zip/gzip files downloaded.
1135 1136 1137 1138 1139 1140 1141 1142
  void removeDownloadedFiles() {
    for (final File file in downloadedFiles) {
      if (!file.existsSync()) {
        continue;
      }
      try {
        file.deleteSync();
      } on FileSystemException catch (e) {
1143
        _logger.printWarning('Failed to delete "${file.path}". Please delete manually. $e');
1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167
        continue;
      }
      for (Directory directory = file.parent; directory.absolute.path != _tempStorage.absolute.path; directory = directory.parent) {
        if (directory.listSync().isNotEmpty) {
          break;
        }
        _deleteIgnoringErrors(directory);
      }
    }
  }

  static void _deleteIgnoringErrors(FileSystemEntity entity) {
    if (!entity.existsSync()) {
      return;
    }
    try {
      entity.deleteSync();
    } on FileSystemException {
      // Ignore errors.
    }
  }
}

@visibleForTesting
1168
String flattenNameSubdirs(Uri url, FileSystem fileSystem) {
1169 1170 1171 1172
  final List<String> pieces = <String>[url.host, ...url.pathSegments];
  final Iterable<String> convertedPieces = pieces.map<String>(_flattenNameNoSubdirs);
  return fileSystem.path.joinAll(convertedPieces);
}
1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197

/// Given a name containing slashes, colons, and backslashes, expand it into
/// something that doesn't.
String _flattenNameNoSubdirs(String fileName) {
  final List<int> replacedCodeUnits = <int>[
    for (int codeUnit in fileName.codeUnits)
      ..._flattenNameSubstitutions[codeUnit] ?? <int>[codeUnit],
  ];
  return String.fromCharCodes(replacedCodeUnits);
}

// Many characters are problematic in filenames, especially on Windows.
final Map<int, List<int>> _flattenNameSubstitutions = <int, List<int>>{
  r'@'.codeUnitAt(0): '@@'.codeUnits,
  r'/'.codeUnitAt(0): '@s@'.codeUnits,
  r'\'.codeUnitAt(0): '@bs@'.codeUnits,
  r':'.codeUnitAt(0): '@c@'.codeUnits,
  r'%'.codeUnitAt(0): '@per@'.codeUnits,
  r'*'.codeUnitAt(0): '@ast@'.codeUnits,
  r'<'.codeUnitAt(0): '@lt@'.codeUnits,
  r'>'.codeUnitAt(0): '@gt@'.codeUnits,
  r'"'.codeUnitAt(0): '@q@'.codeUnits,
  r'|'.codeUnitAt(0): '@pip@'.codeUnits,
  r'?'.codeUnitAt(0): '@ques@'.codeUnits,
};