cache.dart 38.6 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 131
  /// 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".
  @visibleForTesting
  factory Cache.test({
132 133 134 135 136 137
    Directory? rootOverride,
    List<ArtifactSet>? artifacts,
    Logger? logger,
    FileSystem? fileSystem,
    Platform? platform,
    required ProcessManager processManager,
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156
  }) {
    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,
      ),
    );
  }

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

166
  ArtifactUpdater get _artifactUpdater => __artifactUpdater ??= _createUpdater();
167
  ArtifactUpdater? __artifactUpdater;
168

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

174 175 176 177 178 179 180
  /// 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(),
181 182
      platform: _platform,
      httpClient: HttpClient(),
183 184 185
    );
  }

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

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

194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
  /// 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({
215 216 217
    required Platform platform,
    required FileSystem fileSystem,
    required UserMessages userMessages,
218 219 220 221 222
  }) {
    String normalize(String path) {
      return fileSystem.path.normalize(fileSystem.path.absolute(path));
    }
    if (platform.environment.containsKey(kFlutterRootEnvironmentVariableName)) {
223
      return normalize(platform.environment[kFlutterRootEnvironmentVariableName]!);
224 225 226 227 228 229 230 231
    }
    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') {
232
        final String packageConfigPath = Uri.parse(platform.packageConfig!).toFilePath(
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255
          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.
      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. It's not really an "error" though,
          // so print it in grey.
          _logger.printError(
            'Waiting for another flutter command to release the startup lock...',
            color: TerminalColor.grey,
          );
331 332
          printed = true;
        }
333
        await Future<void>.delayed(const Duration(milliseconds: 50));
334 335 336 337
      }
    }
  }

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

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

360
  /// The current version of Dart used to build Flutter and run the tool.
361 362 363 364
  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)'
365
      final String justVersion = _platform.version.split(' ')[0];
366
      _dartSdkVersion = justVersion.replaceFirstMapped(RegExp(r'(\d+\.\d+\.\d+)(.+)'), (Match match) {
367
        final String noFlutter = match[2]!.replaceAll('.flutter-', ' ');
368 369 370
        return '${match[1]} (build ${match[1]}$noFlutter)';
      });
    }
371
    return _dartSdkVersion!;
372
  }
373
  String? _dartSdkVersion;
374

375
  /// The current version of the Flutter engine the flutter tool will download.
376 377
  String get engineRevision {
    _engineRevision ??= getVersionFor('engine');
378 379 380 381
    if (_engineRevision == null) {
      throwToolExit('Could not determine engine revision.');
    }
    return _engineRevision!;
382
  }
383
  String? _engineRevision;
384

385
  String get storageBaseUrl {
386
    final String? overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405
    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;
    }
406
    _logger.printStatus(
407 408 409 410 411 412
      'Flutter assets will be downloaded from $overrideUrl. Make sure you trust this source!',
      emphasis: true,
    );
    _hasWarnedAboutStorageOverride = true;
  }

413
  /// Return the top-level directory in the cache; this is `bin/cache`.
414
  Directory getRoot() {
415
    if (_rootOverride != null) {
416
      return _fileSystem.directory(_fileSystem.path.join(_rootOverride!.path, 'bin', 'cache'));
417
    } else {
418
      return _fileSystem.directory(_fileSystem.path.join(flutterRoot!, 'bin', 'cache'));
419
    }
420
  }
421

422 423 424 425
  String getHostPlatformArchName() {
    return getNameForHostPlatformArch(_osUtils.hostPlatform);
  }

426 427
  /// Return a directory in the cache dir. For `pkg`, this will return `bin/cache/pkg`.
  Directory getCacheDir(String name) {
428
    final Directory dir = _fileSystem.directory(_fileSystem.path.join(getRoot().path, name));
429
    if (!dir.existsSync()) {
430
      dir.createSync(recursive: true);
431
      _osUtils.chmod(dir, '755');
432
    }
433
    return dir;
434 435
  }

436 437 438
  /// Return the top-level directory for artifact downloads.
  Directory getDownloadDir() => getCacheDir('downloads');

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

442
  /// Location of LICENSE file.
443
  File getLicenseFile() => _fileSystem.file(_fileSystem.path.join(flutterRoot!, 'LICENSE'));
444

445 446 447
  /// 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) {
448
    return getCacheArtifacts().childDirectory(name);
449 450
  }

451 452
  MapEntry<String, String> get dyLdLibEntry {
    if (_dyLdLibEntry != null) {
453
      return _dyLdLibEntry!;
454 455
    }
    final List<String> paths = <String>[];
456
    for (final ArtifactSet artifact in _artifacts) {
457 458 459 460
      final Map<String, String> env = artifact.environment;
      if (env == null || !env.containsKey('DYLD_LIBRARY_PATH')) {
        continue;
      }
461
      final String path = env['DYLD_LIBRARY_PATH']!;
462 463
      if (path.isEmpty) {
        continue;
464
      }
465
      paths.add(path);
466 467
    }
    _dyLdLibEntry = MapEntry<String, String>('DYLD_LIBRARY_PATH', paths.join(':'));
468
    return _dyLdLibEntry!;
469
  }
470
  MapEntry<String, String>? _dyLdLibEntry;
471

472 473 474 475 476 477
  /// 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');
  }

478
  String? getVersionFor(String artifactName) {
479
    final File versionFile = _fileSystem.file(_fileSystem.path.join(
480
      _rootOverride?.path ?? flutterRoot!,
481 482 483 484
      'bin',
      'internal',
      '$artifactName.version',
    ));
485 486 487
    return versionFile.existsSync() ? versionFile.readAsStringSync().trim() : null;
  }

488
  /// Delete all stamp files maintained by the cache.
489 490 491 492 493
  void clearStampFiles() {
    try {
      getStampFileFor('flutter_tools').deleteSync();
      for (final ArtifactSet artifact in _artifacts) {
        final File file = getStampFileFor(artifact.stampName);
494
        ErrorHandlingFileSystem.deleteIfExists(file);
495 496 497 498 499 500
      }
    } on FileSystemException catch (err) {
      _logger.printError('Failed to delete some stamp files: $err');
    }
  }

501 502 503
  /// Read the stamp for [artifactName].
  ///
  /// If the file is missing or cannot be parsed, returns `null`.
504
  String? getStampFor(String artifactName) {
505
    final File stampFile = getStampFileFor(artifactName);
506 507 508 509 510 511 512 513
    if (!stampFile.existsSync()) {
      return null;
    }
    try {
      return stampFile.readAsStringSync().trim();
    } on FileSystemException {
      return null;
    }
514 515
  }

516 517 518 519 520
  void setStampFor(String artifactName, String version) {
    getStampFileFor(artifactName).writeAsStringSync(version);
  }

  File getStampFileFor(String artifactName) {
521
    return _fileSystem.file(_fileSystem.path.join(getRoot().path, '$artifactName.stamp'));
522 523
  }

524 525 526
  /// Returns `true` if either [entity] is older than the tools stamp or if
  /// [entity] doesn't exist.
  bool isOlderThanToolsStamp(FileSystemEntity entity) {
527
    final File flutterToolsStamp = getStampFileFor('flutter_tools');
528
    return _fsUtils.isOlderThanReference(
529 530 531
      entity: entity,
      referenceFile: flutterToolsStamp,
    );
532 533
  }

534 535
  Future<bool> isUpToDate() async {
    for (final ArtifactSet artifact in _artifacts) {
536
      if (!await artifact.isUpToDate(_fileSystem)) {
537 538 539 540 541
        return false;
      }
    }
    return true;
  }
542

543 544 545
  /// Update the cache to contain all `requiredArtifacts`.
  Future<void> updateAll(Set<DevelopmentArtifact> requiredArtifacts) async {
    if (!_lockEnabled) {
546
      return;
547
    }
548
    for (final ArtifactSet artifact in _artifacts) {
549
      if (!requiredArtifacts.contains(artifact.developmentArtifact)) {
550
        _logger.printTrace('Artifact $artifact is not required, skipping update.');
551
        continue;
552
      }
553
      if (await artifact.isUpToDate(_fileSystem)) {
554 555 556
        continue;
      }
      try {
557
        await artifact.update(_artifactUpdater, _logger, _fileSystem, _osUtils);
558 559
      } on SocketException catch (e) {
        if (_hostsBlockedInChina.contains(e.address?.host)) {
560
          _logger.printError(
561
            'Failed to retrieve Flutter tool dependencies: ${e.message}.\n'
562
            "If you're in China, please see this page: "
563 564 565 566 567
            'https://flutter.dev/community/china',
            emphasis: true,
          );
        }
        rethrow;
568
      }
569 570
    }
  }
571 572

  Future<bool> areRemoteArtifactsAvailable({
573
    String? engineVersion,
574 575
    bool includeAllPlatforms = true,
  }) async {
576
    final bool includeAllPlatformsState = this.includeAllPlatforms;
577
    bool allAvailable = true;
578
    this.includeAllPlatforms = includeAllPlatforms;
579
    for (final ArtifactSet cachedArtifact in _artifacts) {
580
      if (cachedArtifact is EngineCachedArtifact) {
581
        allAvailable &= await cachedArtifact.checkForArtifacts(engineVersion);
582 583
      }
    }
584
    this.includeAllPlatforms = includeAllPlatformsState;
585
    return allAvailable;
586
  }
587 588

  Future<bool> doesRemoteExist(String message, Uri url) async {
589
    final Status status = _logger.startProgress(
590 591 592 593 594 595 596 597 598 599
      message,
    );
    bool exists;
    try {
      exists = await _net.doesRemoteFileExist(url);
    } finally {
      status.stop();
    }
    return exists;
  }
600 601
}

602 603 604 605 606 607 608 609
/// 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.
610
  Future<bool> isUpToDate(FileSystem fileSystem);
611 612 613 614 615 616 617

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

  /// Updates the artifact.
618 619 620 621 622 623
  Future<void> update(
    ArtifactUpdater artifactUpdater,
    Logger logger,
    FileSystem fileSystem,
    OperatingSystemUtils operatingSystemUtils,
  );
624 625 626 627 628 629 630

  /// 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;
631 632 633 634 635 636 637 638 639
}

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

641
  final Cache cache;
642

643
  @override
644 645
  final String name;

646
  @override
647 648
  String get stampName => name;

649
  Directory get location => cache.getArtifactDirectory(name);
650 651

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

653 654 655
  // Whether or not to bypass normal platform filtering for this artifact.
  bool get ignorePlatformFiltering {
    return cache.includeAllPlatforms ||
656
      (cache.platformOverrideArtifacts != null && cache.platformOverrideArtifacts!.contains(developmentArtifact.name));
657 658
  }

659
  @override
660
  Future<bool> isUpToDate(FileSystem fileSystem) async {
661
    if (!location.existsSync()) {
662
      return false;
663 664
    }
    if (version != cache.getStampFor(stampName)) {
665
      return false;
666
    }
667
    return isUpToDateInner(fileSystem);
668 669
  }

670
  @override
671 672 673 674 675 676
  Future<void> update(
    ArtifactUpdater artifactUpdater,
    Logger logger,
    FileSystem fileSystem,
    OperatingSystemUtils operatingSystemUtils,
  ) async {
677
    if (!location.existsSync()) {
678 679 680
      try {
        location.createSync(recursive: true);
      } on FileSystemException catch (err) {
681
        logger.printError(err.toString());
682 683 684 685 686
        throwToolExit(
          'Failed to create directory for flutter cache at ${location.path}. '
          'Flutter may be missing permissions in its cache directory.'
        );
      }
687
    }
688
    await updateInner(artifactUpdater, fileSystem, operatingSystemUtils);
689
    try {
690 691 692 693 694 695 696 697 698
      if (version == null) {
        logger.printError(
          '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!);
      }
699
    } on FileSystemException catch (err) {
700
      logger.printError(
701 702 703 704 705 706
        '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.',
      );
    }
707
    artifactUpdater.removeDownloadedFiles();
708 709
  }

710
  /// Hook method for extra checks for being up-to-date.
711
  bool isUpToDateInner(FileSystem fileSystem) => true;
712

713 714 715 716 717
  Future<void> updateInner(
    ArtifactUpdater artifactUpdater,
    FileSystem fileSystem,
    OperatingSystemUtils operatingSystemUtils,
  );
718
}
719

720

721 722 723 724
abstract class EngineCachedArtifact extends CachedArtifact {
  EngineCachedArtifact(
    this.stampName,
    Cache cache,
725 726
    DevelopmentArtifact developmentArtifact,
  ) : super('engine', cache, developmentArtifact);
727

728 729
  @override
  final String stampName;
730

731 732
  /// Return a list of (directory path, download URL path) tuples.
  List<List<String>> getBinaryDirs();
733

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

737 738
  /// A list of the dart package directories to download.
  List<String> getPackageDirs();
739

740
  @override
741
  bool isUpToDateInner(FileSystem fileSystem) {
742
    final Directory pkgDir = cache.getCacheDir('pkg');
743
    for (final String pkgName in getPackageDirs()) {
744 745
      final String pkgPath = fileSystem.path.join(pkgDir.path, pkgName);
      if (!fileSystem.directory(pkgPath).existsSync()) {
746
        return false;
747
      }
748
    }
749

750
    for (final List<String> toolsDir in getBinaryDirs()) {
751
      final Directory dir = fileSystem.directory(fileSystem.path.join(location.path, toolsDir[0]));
752
      if (!dir.existsSync()) {
753
        return false;
754
      }
755
    }
756

757
    for (final String licenseDir in getLicenseDirs()) {
758
      final File file = fileSystem.file(fileSystem.path.join(location.path, licenseDir, 'LICENSE'));
759
      if (!file.existsSync()) {
760
        return false;
761
      }
762 763
    }
    return true;
764 765
  }

766
  @override
767 768 769 770 771
  Future<void> updateInner(
    ArtifactUpdater artifactUpdater,
    FileSystem fileSystem,
    OperatingSystemUtils operatingSystemUtils,
  ) async {
772
    final String url = '${cache.storageBaseUrl}/flutter_infra_release/flutter/$version/';
773 774

    final Directory pkgDir = cache.getCacheDir('pkg');
775
    for (final String pkgName in getPackageDirs()) {
776
      await artifactUpdater.downloadZipArchive('Downloading package $pkgName...', Uri.parse('$url$pkgName.zip'), pkgDir);
777 778
    }

779
    for (final List<String> toolsDir in getBinaryDirs()) {
780 781
      final String cacheDir = toolsDir[0];
      final String urlPath = toolsDir[1];
782
      final Directory dir = fileSystem.directory(fileSystem.path.join(location.path, cacheDir));
783 784 785

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

788
      _makeFilesExecutable(dir, operatingSystemUtils);
789

790 791 792
      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'));
793
        ErrorHandlingFileSystem.deleteIfExists(framework, recursive: true);
794 795
        framework.createSync();
        operatingSystemUtils.unzip(frameworkZip, framework);
Devon Carew's avatar
Devon Carew committed
796 797
      }
    }
798

799
    final File licenseSource = cache.getLicenseFile();
800
    for (final String licenseDir in getLicenseDirs()) {
801
      final String licenseDestinationPath = fileSystem.path.join(location.path, licenseDir, 'LICENSE');
802 803
      await licenseSource.copy(licenseDestinationPath);
    }
804 805
  }

806
  Future<bool> checkForArtifacts(String? engineVersion) async {
807
    engineVersion ??= version;
808
    final String url = '${cache.storageBaseUrl}/flutter_infra_release/flutter/$engineVersion/';
809

810
    bool exists = false;
811
    for (final String pkgName in getPackageDirs()) {
812
      exists = await cache.doesRemoteExist('Checking package $pkgName is available...', Uri.parse('$url$pkgName.zip'));
813 814
      if (!exists) {
        return false;
815
      }
816
    }
817

818
    for (final List<String> toolsDir in getBinaryDirs()) {
819 820
      final String cacheDir = toolsDir[0];
      final String urlPath = toolsDir[1];
821
      exists = await cache.doesRemoteExist('Checking $cacheDir tools are available...',
822 823 824
          Uri.parse(url + urlPath));
      if (!exists) {
        return false;
825
      }
826
    }
827
    return true;
828 829
  }

830 831 832 833 834 835 836 837
  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');
838 839 840
      }
    }
  }
841 842
}

843 844 845 846
/// An API for downloading and un-archiving artifacts, such as engine binaries or
/// additional source code.
class ArtifactUpdater {
  ArtifactUpdater({
847 848 849 850 851 852
    required OperatingSystemUtils operatingSystemUtils,
    required Logger logger,
    required FileSystem fileSystem,
    required Directory tempStorage,
    required HttpClient httpClient,
    required Platform platform,
853
  }) : _operatingSystemUtils = operatingSystemUtils,
854
       _httpClient = httpClient,
855 856
       _logger = logger,
       _fileSystem = fileSystem,
857 858 859 860 861
       _tempStorage = tempStorage,
       _platform = platform;

  /// The number of times the artifact updater will repeat the artifact download loop.
  static const int _kRetryCount = 2;
862 863 864 865 866

  final Logger _logger;
  final OperatingSystemUtils _operatingSystemUtils;
  final FileSystem _fileSystem;
  final Directory _tempStorage;
867 868
  final HttpClient _httpClient;
  final Platform _platform;
869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909

  /// 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);
910
    Status status;
911
    int retries = _kRetryCount;
912 913

    while (retries > 0) {
914 915 916
      status = _logger.startProgress(
        message,
      );
917 918
      try {
        _ensureExists(tempFile.parent);
919 920 921 922 923
        if (tempFile.existsSync()) {
          tempFile.deleteSync();
        }
        await _download(url, tempFile);

924 925 926
        if (!tempFile.existsSync()) {
          throw Exception('Did not find downloaded file ${tempFile.path}');
        }
927 928 929 930 931
      } on Exception catch (err) {
        _logger.printTrace(err.toString());
        retries -= 1;
        if (retries == 0) {
          throwToolExit(
932
            'Failed to download $url. Ensure you have network connectivity and then try again.\n$err',
933 934 935 936
          );
        }
        continue;
      } on ArgumentError catch (error) {
937
        final String? overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
938 939 940 941 942 943 944 945 946 947 948 949 950
        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;
951 952 953
      } finally {
        status.stop();
      }
954 955 956 957 958
      /// 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)
      );
959 960 961 962 963 964 965 966 967 968
      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;
969
        if (_platform.isWindows && error.osError?.errorCode == kSharingViolation) {
970 971 972 973 974 975 976
          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'
          );
        }
      }
977 978 979 980
      _ensureExists(location);

      try {
        extractor(tempFile, location);
981
      } on Exception catch (err) {
982 983
        retries -= 1;
        if (retries == 0) {
984 985
          throwToolExit(
            'Flutter could not download and/or extract $url. Ensure you have '
986
            'network connectivity and all of the required dependencies listed at '
987 988
            'flutter.dev/setup.\nThe original exception was: $err.'
          );
989 990 991
        }
        _deleteIgnoringErrors(tempFile);
        continue;
992 993 994 995 996
      }
      return;
    }
  }

997
  /// Download bytes from [url], throwing non-200 responses as an exception.
998 999 1000 1001 1002 1003 1004
  ///
  /// 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
1005
  Future<void> _download(Uri url, File file) async {
1006 1007 1008 1009 1010
    final HttpClientRequest request = await _httpClient.getUrl(url);
    final HttpClientResponse response = await request.close();
    if (response.statusCode != HttpStatus.ok) {
      throw Exception(response.statusCode);
    }
1011

1012 1013 1014
    final String? md5Hash = _expectedMd5(response.headers);
    ByteConversionSink? inputSink;
    late StreamController<Digest> digests;
1015 1016 1017 1018 1019 1020
    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);
1021
    await response.forEach((List<int> chunk) {
1022 1023
      inputSink?.add(chunk);
      randomAccessFile.writeFromSync(chunk);
1024
    });
1025 1026 1027 1028 1029 1030
    randomAccessFile.closeSync();
    if (inputSink != null) {
      inputSink.close();
      final Digest digest = await digests.stream.last;
      final String rawDigest = base64.encode(digest.bytes);
      if (rawDigest != md5Hash) {
1031
        throw Exception(
1032 1033 1034 1035 1036 1037 1038 1039 1040
          '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.'
        );
      }
    }
  }

1041 1042
  String? _expectedMd5(HttpHeaders httpHeaders) {
    final List<String>? values = httpHeaders['x-goog-hash'];
1043 1044 1045
    if (values == null) {
      return null;
    }
1046 1047 1048 1049 1050 1051 1052
    String? rawMd5Hash;
    for (final String value in values) {
      if (value.startsWith('md5=')) {
        rawMd5Hash = value;
        break;
      }
    }
1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064
    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;
1065 1066
  }

1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081
  /// 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);
    }
  }

1082
  /// Clear any zip/gzip files downloaded.
1083 1084 1085 1086 1087 1088 1089 1090
  void removeDownloadedFiles() {
    for (final File file in downloadedFiles) {
      if (!file.existsSync()) {
        continue;
      }
      try {
        file.deleteSync();
      } on FileSystemException catch (e) {
1091
        _logger.printError('Failed to delete "${file.path}". Please delete manually. $e');
1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115
        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
1116
String flattenNameSubdirs(Uri url, FileSystem fileSystem) {
1117 1118 1119 1120
  final List<String> pieces = <String>[url.host, ...url.pathSegments];
  final Iterable<String> convertedPieces = pieces.map<String>(_flattenNameNoSubdirs);
  return fileSystem.path.joinAll(convertedPieces);
}
1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145

/// 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,
};