cache.dart 44.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
  /// Artifacts required for any development platform.
74 75 76
  ///
  /// This does not need to be explicitly returned from requiredArtifacts as
  /// it will always be downloaded.
77 78
  static const DevelopmentArtifact universal = DevelopmentArtifact._('universal');

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

  @override
95
  String toString() => 'Artifact($name)';
96 97
}

98
/// A wrapper around the `bin/cache/` directory.
99 100 101
///
/// This does not provide any artifacts by default. See [FlutterCache] for the default
/// artifact set.
102 103 104 105 106 107 108 109 110 111 112 113 114 115
///
/// ## Artifact mirrors
///
/// Some environments cannot reach the Google Cloud Storage buckets and CIPD due
/// to regional or corporate policies.
///
/// To enable Flutter users in these environments, the Flutter tool supports
/// custom artifact mirrors that the administrators of such environments may
/// provide. To use an artifact mirror, the user defines the
/// `FLUTTER_STORAGE_BASE_URL` environment variable that points to the mirror.
/// Flutter tool reads this variable and uses it instead of the default URLs.
///
/// For more details on specific URLs used to download artifacts, see
/// [storageBaseUrl] and [cipdBaseUrl].
116
class Cache {
117
  /// [rootOverride] is configurable for testing.
118
  /// [artifacts] is configurable for testing.
119
  Cache({
120 121 122 123 124 125
    @protected Directory? rootOverride,
    @protected List<ArtifactSet>? artifacts,
    required Logger logger,
    required FileSystem fileSystem,
    required Platform platform,
    required OperatingSystemUtils osUtils,
126
  }) : _rootOverride = rootOverride,
127 128 129 130 131
       _logger = logger,
       _fileSystem = fileSystem,
       _platform = platform,
       _osUtils = osUtils,
      _net = Net(logger: logger, platform: platform),
132
      _fsUtils = FileSystemUtils(fileSystem: fileSystem, platform: platform),
133
      _artifacts = artifacts ?? <ArtifactSet>[];
134

135 136 137 138 139 140
  /// 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({
141 142 143 144 145 146
    Directory? rootOverride,
    List<ArtifactSet>? artifacts,
    Logger? logger,
    FileSystem? fileSystem,
    Platform? platform,
    required ProcessManager processManager,
147 148 149 150 151
  }) {
    fileSystem ??= rootOverride?.fileSystem ?? MemoryFileSystem.test();
    platform ??= FakePlatform(environment: <String, String>{});
    logger ??= BufferLogger.test();
    return Cache(
152
      rootOverride: rootOverride ?? fileSystem.directory('cache'),
153 154 155 156 157 158 159 160 161 162 163 164 165
      artifacts: artifacts ?? <ArtifactSet>[],
      logger: logger,
      fileSystem: fileSystem,
      platform: platform,
      osUtils: OperatingSystemUtils(
        fileSystem: fileSystem,
        logger: logger,
        platform: platform,
        processManager: processManager,
      ),
    );
  }

166 167 168 169
  final Logger _logger;
  final Platform _platform;
  final FileSystem _fileSystem;
  final OperatingSystemUtils _osUtils;
170
  final Directory? _rootOverride;
171 172 173
  final List<ArtifactSet> _artifacts;
  final Net _net;
  final FileSystemUtils _fsUtils;
174

175
  late final ArtifactUpdater _artifactUpdater = _createUpdater();
176

177
  @visibleForTesting
178 179 180 181 182
  @protected
  void registerArtifact(ArtifactSet artifactSet) {
    _artifacts.add(artifactSet);
  }

183 184 185 186 187 188 189
  /// 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(),
190 191
      platform: _platform,
      httpClient: HttpClient(),
192 193 194 195
      allowedBaseUrls: <String>[
        storageBaseUrl,
        cipdBaseUrl,
      ],
196 197 198
    );
  }

199
  static const List<String> _hostsBlockedInChina = <String> [
200
    'storage.googleapis.com',
201
    'chrome-infra-packages.appspot.com',
202 203
  ];

204
  // Initialized by FlutterCommandRunner on startup.
205 206
  // Explore making this field lazy to catch non-initialized access.
  static String? flutterRoot;
207

208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228
  /// 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({
229 230 231
    required Platform platform,
    required FileSystem fileSystem,
    required UserMessages userMessages,
232 233 234 235 236
  }) {
    String normalize(String path) {
      return fileSystem.path.normalize(fileSystem.path.absolute(path));
    }
    if (platform.environment.containsKey(kFlutterRootEnvironmentVariableName)) {
237
      return normalize(platform.environment[kFlutterRootEnvironmentVariableName]!);
238 239 240 241 242 243 244 245
    }
    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') {
246
        final String packageConfigPath = Uri.parse(platform.packageConfig!).toFilePath(
247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264
          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.
265
      // ignore: avoid_print
266 267 268 269 270
      print(userMessages.runnerNoRoot('$error'));
    }
    return normalize('.');
  }

271 272 273 274
  // Whether to cache artifacts for all platforms. Defaults to only caching
  // artifacts for the current platform.
  bool includeAllPlatforms = false;

275 276
  // Names of artifacts which should be cached even if they would normally
  // be filtered out for the current platform.
277
  Set<String>? platformOverrideArtifacts;
278

279 280 281
  // Whether to cache the unsigned mac binaries. Defaults to caching the signed binaries.
  bool useUnsignedMacBinaries = false;

282
  static RandomAccessFile? _lock;
283 284
  static bool _lockEnabled = true;

285
  /// Turn off the [lock]/[releaseLock] mechanism.
286 287 288
  ///
  /// 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.
289
  @visibleForTesting
290 291 292 293
  static void disableLocking() {
    _lockEnabled = false;
  }

294
  /// Turn on the [lock]/[releaseLock] mechanism.
295 296 297 298 299 300 301
  ///
  /// This is used by the tests.
  @visibleForTesting
  static void enableLocking() {
    _lockEnabled = true;
  }

302 303 304 305 306 307 308 309
  /// Check if lock acquired, skipping FLUTTER_ALREADY_LOCKED reentrant checks.
  ///
  /// This is used by the tests.
  @visibleForTesting
  static bool isLocked() {
    return _lock != null;
  }

310 311
  /// Lock the cache directory.
  ///
312 313
  /// This happens while required artifacts are updated
  /// (see [FlutterCommandRunner.runCommand]).
314
  ///
315
  /// This uses normal POSIX flock semantics.
316
  Future<void> lock() async {
317
    if (!_lockEnabled) {
318
      return;
319
    }
320
    assert(_lock == null);
321
    final File lockFile =
322
      _fileSystem.file(_fileSystem.path.join(flutterRoot!, 'bin', 'cache', 'lockfile'));
323 324 325
    try {
      _lock = lockFile.openSync(mode: FileMode.write);
    } on FileSystemException catch (e) {
326 327
      _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}');
328 329
      throwToolExit('Failed to open or create the lockfile');
    }
330 331 332 333
    bool locked = false;
    bool printed = false;
    while (!locked) {
      try {
334
        _lock!.lockSync();
335 336 337
        locked = true;
      } on FileSystemException {
        if (!printed) {
338
          _logger.printTrace('Waiting to be able to obtain lock of Flutter binary artifacts directory: ${_lock!.path}');
339 340 341 342 343 344 345
          // 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(
346 347 348
            'Waiting for another flutter command to release the startup lock...',
            color: TerminalColor.grey,
          );
349
          _logger.hadWarningOutput = oldWarnings;
350 351
          printed = true;
        }
352
        await Future<void>.delayed(const Duration(milliseconds: 50));
353 354 355 356
      }
    }
  }

357 358 359 360
  /// Releases the lock.
  ///
  /// This happens automatically on startup (see [FlutterCommand.verifyThenRunCommand])
  /// after the command's required artifacts are updated.
361
  void releaseLock() {
362
    if (!_lockEnabled || _lock == null) {
363
      return;
364
    }
365
    _lock!.closeSync();
366 367 368
    _lock = null;
  }

369 370
  /// Checks if the current process owns the lock for the cache directory at
  /// this very moment; throws a [StateError] if it doesn't.
371 372
  void checkLockAcquired() {
    if (_lockEnabled && _lock == null && _platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') {
373
      throw StateError(
374 375 376 377 378
        'The current process does not own the lock for the cache directory. This is a bug in Flutter CLI tools.',
      );
    }
  }

379 380 381 382 383 384 385 386 387 388 389 390 391
  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());
392 393
      if (data is! Map<String, Object?>) {
        throw Exception("Expected object of type 'Map<String, Object?>' but got one of type '${data.runtimeType}'");
394
      }
395
      final Object? version = data['version'];
396 397 398 399 400 401 402 403 404 405 406 407
      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;

408
  /// The current version of Dart used to build Flutter and run the tool.
409 410 411 412
  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)'
413
      final String justVersion = _platform.version.split(' ')[0];
414
      _dartSdkVersion = justVersion.replaceFirstMapped(RegExp(r'(\d+\.\d+\.\d+)(.+)'), (Match match) {
415
        final String noFlutter = match[2]!.replaceAll('.flutter-', ' ');
416 417 418
        return '${match[1]} (build ${match[1]}$noFlutter)';
      });
    }
419
    return _dartSdkVersion!;
420
  }
421
  String? _dartSdkVersion;
422

423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438
  /// 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;


439
  /// The current version of the Flutter engine the flutter tool will download.
440 441
  String get engineRevision {
    _engineRevision ??= getVersionFor('engine');
442 443 444 445
    if (_engineRevision == null) {
      throwToolExit('Could not determine engine revision.');
    }
    return _engineRevision!;
446
  }
447
  String? _engineRevision;
448

449 450 451 452 453 454 455 456 457 458 459
  /// The base for URLs that store Flutter engine artifacts that are fetched
  /// during the installation of the Flutter SDK.
  ///
  /// By default the base URL is https://storage.googleapis.com. However, if
  /// `FLUTTER_STORAGE_BASE_URL` environment variable is provided, the
  /// environment variable value is returned instead.
  ///
  /// See also:
  ///
  ///  * [cipdBaseUrl], which determines how CIPD artifacts are fetched.
  ///  * [Cache] class-level dartdocs that explain how artifact mirrors work.
460
  String get storageBaseUrl {
461
    final String? overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
462 463 464 465 466 467 468 469 470 471 472 473 474
    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;
  }

475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493
  /// The base for URLs that store Flutter engine artifacts in CIPD.
  ///
  /// For some platforms, such as Web and Fuchsia, CIPD artifacts are fetched
  /// during the installation of the Flutter SDK, in addition to those fetched
  /// from [storageBaseUrl].
  ///
  /// By default the base URL is https://chrome-infra-packages.appspot.com/dl.
  /// However, if `FLUTTER_STORAGE_BASE_URL` environment variable is provided,
  /// then the following value is used:
  ///
  ///     FLUTTER_STORAGE_BASE_URL/flutter_infra_release/cipd
  ///
  /// See also:
  ///
  ///  * [storageBaseUrl], which determines how engine artifacts stored in the
  ///    Google Cloud Storage buckets are fetched.
  ///  * https://chromium.googlesource.com/infra/luci/luci-go/+/refs/heads/main/cipd,
  ///    which contains information about CIPD.
  ///  * [Cache] class-level dartdocs that explain how artifact mirrors work.
494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516
  String get cipdBaseUrl {
    final String? overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
    if (overrideUrl == null) {
      return 'https://chrome-infra-packages.appspot.com/dl';
    }

    final Uri original;
    try {
      original = Uri.parse(overrideUrl);
    } on FormatException catch (err) {
      throwToolExit('"FLUTTER_STORAGE_BASE_URL" contains an invalid URI:\n$err');
    }

    final String cipdOverride = original.replace(
      pathSegments: <String>[
        ...original.pathSegments,
        'flutter_infra_release',
        'cipd',
      ],
    ).toString();
    return cipdOverride;
  }

517 518 519 520 521 522
  bool _hasWarnedAboutStorageOverride = false;

  void _maybeWarnAboutStorageOverride(String overrideUrl) {
    if (_hasWarnedAboutStorageOverride) {
      return;
    }
523
    _logger.printError(
524 525 526 527 528 529
      'Flutter assets will be downloaded from $overrideUrl. Make sure you trust this source!',
      emphasis: true,
    );
    _hasWarnedAboutStorageOverride = true;
  }

530
  /// Return the top-level directory in the cache; this is `bin/cache`.
531
  Directory getRoot() {
532
    if (_rootOverride != null) {
533
      return _fileSystem.directory(_fileSystem.path.join(_rootOverride!.path, 'bin', 'cache'));
534
    } else {
535
      return _fileSystem.directory(_fileSystem.path.join(flutterRoot!, 'bin', 'cache'));
536
    }
537
  }
538

539 540 541 542
  String getHostPlatformArchName() {
    return getNameForHostPlatformArch(_osUtils.hostPlatform);
  }

543
  /// Return a directory in the cache dir. For `pkg`, this will return `bin/cache/pkg`.
544 545 546 547
  ///
  /// 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 }) {
548
    final Directory dir = _fileSystem.directory(_fileSystem.path.join(getRoot().path, name));
549
    if (!dir.existsSync() && shouldCreate) {
550
      dir.createSync(recursive: true);
551
      _osUtils.chmod(dir, '755');
552
    }
553
    return dir;
554 555
  }

556 557 558
  /// Return the top-level directory for artifact downloads.
  Directory getDownloadDir() => getCacheDir('downloads');

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

562
  /// Location of LICENSE file.
563
  File getLicenseFile() => _fileSystem.file(_fileSystem.path.join(flutterRoot!, 'LICENSE'));
564

565 566 567
  /// 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) {
568
    return getCacheArtifacts().childDirectory(name);
569 570
  }

571 572
  MapEntry<String, String> get dyLdLibEntry {
    if (_dyLdLibEntry != null) {
573
      return _dyLdLibEntry!;
574 575
    }
    final List<String> paths = <String>[];
576
    for (final ArtifactSet artifact in _artifacts) {
577 578 579 580
      final Map<String, String> env = artifact.environment;
      if (env == null || !env.containsKey('DYLD_LIBRARY_PATH')) {
        continue;
      }
581
      final String path = env['DYLD_LIBRARY_PATH']!;
582 583
      if (path.isEmpty) {
        continue;
584
      }
585
      paths.add(path);
586 587
    }
    _dyLdLibEntry = MapEntry<String, String>('DYLD_LIBRARY_PATH', paths.join(':'));
588
    return _dyLdLibEntry!;
589
  }
590
  MapEntry<String, String>? _dyLdLibEntry;
591

592 593 594 595 596 597
  /// 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');
  }

598
  String? getVersionFor(String artifactName) {
599
    final File versionFile = _fileSystem.file(_fileSystem.path.join(
600
      _rootOverride?.path ?? flutterRoot!,
601 602 603 604
      'bin',
      'internal',
      '$artifactName.version',
    ));
605 606 607
    return versionFile.existsSync() ? versionFile.readAsStringSync().trim() : null;
  }

608
  /// Delete all stamp files maintained by the cache.
609 610 611 612 613
  void clearStampFiles() {
    try {
      getStampFileFor('flutter_tools').deleteSync();
      for (final ArtifactSet artifact in _artifacts) {
        final File file = getStampFileFor(artifact.stampName);
614
        ErrorHandlingFileSystem.deleteIfExists(file);
615 616
      }
    } on FileSystemException catch (err) {
617
      _logger.printWarning('Failed to delete some stamp files: $err');
618 619 620
    }
  }

621 622 623
  /// Read the stamp for [artifactName].
  ///
  /// If the file is missing or cannot be parsed, returns `null`.
624
  String? getStampFor(String artifactName) {
625
    final File stampFile = getStampFileFor(artifactName);
626 627 628 629 630 631 632 633
    if (!stampFile.existsSync()) {
      return null;
    }
    try {
      return stampFile.readAsStringSync().trim();
    } on FileSystemException {
      return null;
    }
634 635
  }

636 637 638 639 640
  void setStampFor(String artifactName, String version) {
    getStampFileFor(artifactName).writeAsStringSync(version);
  }

  File getStampFileFor(String artifactName) {
641
    return _fileSystem.file(_fileSystem.path.join(getRoot().path, '$artifactName.stamp'));
642 643
  }

644 645 646
  /// Returns `true` if either [entity] is older than the tools stamp or if
  /// [entity] doesn't exist.
  bool isOlderThanToolsStamp(FileSystemEntity entity) {
647
    final File flutterToolsStamp = getStampFileFor('flutter_tools');
648
    return _fsUtils.isOlderThanReference(
649 650 651
      entity: entity,
      referenceFile: flutterToolsStamp,
    );
652 653
  }

654 655
  Future<bool> isUpToDate() async {
    for (final ArtifactSet artifact in _artifacts) {
656
      if (!await artifact.isUpToDate(_fileSystem)) {
657 658 659 660 661
        return false;
      }
    }
    return true;
  }
662

663
  /// Update the cache to contain all `requiredArtifacts`.
664
  Future<void> updateAll(Set<DevelopmentArtifact> requiredArtifacts, {bool offline = false}) async {
665
    if (!_lockEnabled) {
666
      return;
667
    }
668
    for (final ArtifactSet artifact in _artifacts) {
669
      if (!requiredArtifacts.contains(artifact.developmentArtifact)) {
670
        _logger.printTrace('Artifact $artifact is not required, skipping update.');
671
        continue;
672
      }
673
      if (await artifact.isUpToDate(_fileSystem)) {
674 675 676
        continue;
      }
      try {
677
        await artifact.update(_artifactUpdater, _logger, _fileSystem, _osUtils, offline: offline);
678 679
      } on SocketException catch (e) {
        if (_hostsBlockedInChina.contains(e.address?.host)) {
680
          _logger.printError(
681
            'Failed to retrieve Flutter tool dependencies: ${e.message}.\n'
682
            "If you're in China, please see this page: "
683 684 685 686 687
            'https://flutter.dev/community/china',
            emphasis: true,
          );
        }
        rethrow;
688
      }
689 690
    }
  }
691 692

  Future<bool> areRemoteArtifactsAvailable({
693
    String? engineVersion,
694 695
    bool includeAllPlatforms = true,
  }) async {
696
    final bool includeAllPlatformsState = this.includeAllPlatforms;
697
    bool allAvailable = true;
698
    this.includeAllPlatforms = includeAllPlatforms;
699
    for (final ArtifactSet cachedArtifact in _artifacts) {
700
      if (cachedArtifact is EngineCachedArtifact) {
701
        allAvailable &= await cachedArtifact.checkForArtifacts(engineVersion);
702 703
      }
    }
704
    this.includeAllPlatforms = includeAllPlatformsState;
705
    return allAvailable;
706
  }
707 708

  Future<bool> doesRemoteExist(String message, Uri url) async {
709
    final Status status = _logger.startProgress(
710 711 712 713 714 715 716 717 718 719
      message,
    );
    bool exists;
    try {
      exists = await _net.doesRemoteFileExist(url);
    } finally {
      status.stop();
    }
    return exists;
  }
720 721
}

722 723 724 725 726 727 728 729
/// 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.
730
  Future<bool> isUpToDate(FileSystem fileSystem);
731 732 733 734 735 736 737

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

  /// Updates the artifact.
738 739 740 741 742
  Future<void> update(
    ArtifactUpdater artifactUpdater,
    Logger logger,
    FileSystem fileSystem,
    OperatingSystemUtils operatingSystemUtils,
743
    {bool offline = false}
744
  );
745 746 747 748 749 750 751

  /// 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;
752 753 754 755 756 757 758 759 760
}

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

762
  final Cache cache;
763

764
  @override
765 766
  final String name;

767
  @override
768 769
  String get stampName => name;

770
  Directory get location => cache.getArtifactDirectory(name);
771 772

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

774 775 776
  // Whether or not to bypass normal platform filtering for this artifact.
  bool get ignorePlatformFiltering {
    return cache.includeAllPlatforms ||
777
      (cache.platformOverrideArtifacts != null && cache.platformOverrideArtifacts!.contains(developmentArtifact.name));
778 779
  }

780
  @override
781
  Future<bool> isUpToDate(FileSystem fileSystem) async {
782
    if (!location.existsSync()) {
783
      return false;
784 785
    }
    if (version != cache.getStampFor(stampName)) {
786
      return false;
787
    }
788
    return isUpToDateInner(fileSystem);
789 790
  }

791
  @override
792 793 794 795 796
  Future<void> update(
    ArtifactUpdater artifactUpdater,
    Logger logger,
    FileSystem fileSystem,
    OperatingSystemUtils operatingSystemUtils,
797
    {bool offline = false}
798
  ) async {
799
    if (!location.existsSync()) {
800 801 802
      try {
        location.createSync(recursive: true);
      } on FileSystemException catch (err) {
803
        logger.printError(err.toString());
804 805 806 807 808
        throwToolExit(
          'Failed to create directory for flutter cache at ${location.path}. '
          'Flutter may be missing permissions in its cache directory.'
        );
      }
809
    }
810
    await updateInner(artifactUpdater, fileSystem, operatingSystemUtils);
811
    try {
812
      if (version == null) {
813
        logger.printWarning(
814 815 816 817 818 819 820
          '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!);
      }
821
    } on FileSystemException catch (err) {
822
      logger.printWarning(
823 824 825 826 827 828
        '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.',
      );
    }
829
    artifactUpdater.removeDownloadedFiles();
830 831
  }

832
  /// Hook method for extra checks for being up-to-date.
833
  bool isUpToDateInner(FileSystem fileSystem) => true;
834

835 836 837 838 839
  Future<void> updateInner(
    ArtifactUpdater artifactUpdater,
    FileSystem fileSystem,
    OperatingSystemUtils operatingSystemUtils,
  );
840
}
841

842

843 844 845 846
abstract class EngineCachedArtifact extends CachedArtifact {
  EngineCachedArtifact(
    this.stampName,
    Cache cache,
847 848
    DevelopmentArtifact developmentArtifact,
  ) : super('engine', cache, developmentArtifact);
849

850 851
  @override
  final String stampName;
852

853 854
  /// Return a list of (directory path, download URL path) tuples.
  List<List<String>> getBinaryDirs();
855

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

859 860
  /// A list of the dart package directories to download.
  List<String> getPackageDirs();
861

862
  @override
863
  bool isUpToDateInner(FileSystem fileSystem) {
864
    final Directory pkgDir = cache.getCacheDir('pkg');
865
    for (final String pkgName in getPackageDirs()) {
866 867
      final String pkgPath = fileSystem.path.join(pkgDir.path, pkgName);
      if (!fileSystem.directory(pkgPath).existsSync()) {
868
        return false;
869
      }
870
    }
871

872
    for (final List<String> toolsDir in getBinaryDirs()) {
873
      final Directory dir = fileSystem.directory(fileSystem.path.join(location.path, toolsDir[0]));
874
      if (!dir.existsSync()) {
875
        return false;
876
      }
877
    }
878

879
    for (final String licenseDir in getLicenseDirs()) {
880
      final File file = fileSystem.file(fileSystem.path.join(location.path, licenseDir, 'LICENSE'));
881
      if (!file.existsSync()) {
882
        return false;
883
      }
884 885
    }
    return true;
886 887
  }

888
  @override
889 890 891 892 893
  Future<void> updateInner(
    ArtifactUpdater artifactUpdater,
    FileSystem fileSystem,
    OperatingSystemUtils operatingSystemUtils,
  ) async {
894
    final String url = '${cache.storageBaseUrl}/flutter_infra_release/flutter/$version/';
895 896

    final Directory pkgDir = cache.getCacheDir('pkg');
897
    for (final String pkgName in getPackageDirs()) {
898
      await artifactUpdater.downloadZipArchive('Downloading package $pkgName...', Uri.parse('$url$pkgName.zip'), pkgDir);
899 900
    }

901
    for (final List<String> toolsDir in getBinaryDirs()) {
902 903
      final String cacheDir = toolsDir[0];
      final String urlPath = toolsDir[1];
904
      final Directory dir = fileSystem.directory(fileSystem.path.join(location.path, cacheDir));
905 906 907

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

910
      _makeFilesExecutable(dir, operatingSystemUtils);
911

912 913 914
      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'));
915
        ErrorHandlingFileSystem.deleteIfExists(framework, recursive: true);
916 917
        framework.createSync();
        operatingSystemUtils.unzip(frameworkZip, framework);
Devon Carew's avatar
Devon Carew committed
918 919
      }
    }
920

921
    final File licenseSource = cache.getLicenseFile();
922
    for (final String licenseDir in getLicenseDirs()) {
923
      final String licenseDestinationPath = fileSystem.path.join(location.path, licenseDir, 'LICENSE');
924 925
      await licenseSource.copy(licenseDestinationPath);
    }
926 927
  }

928
  Future<bool> checkForArtifacts(String? engineVersion) async {
929
    engineVersion ??= version;
930
    final String url = '${cache.storageBaseUrl}/flutter_infra_release/flutter/$engineVersion/';
931

932
    bool exists = false;
933
    for (final String pkgName in getPackageDirs()) {
934
      exists = await cache.doesRemoteExist('Checking package $pkgName is available...', Uri.parse('$url$pkgName.zip'));
935 936
      if (!exists) {
        return false;
937
      }
938
    }
939

940
    for (final List<String> toolsDir in getBinaryDirs()) {
941 942
      final String cacheDir = toolsDir[0];
      final String urlPath = toolsDir[1];
943
      exists = await cache.doesRemoteExist('Checking $cacheDir tools are available...',
944 945 946
          Uri.parse(url + urlPath));
      if (!exists) {
        return false;
947
      }
948
    }
949
    return true;
950 951
  }

952 953 954 955 956 957 958 959
  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');
960 961 962
      }
    }
  }
963 964
}

965 966 967 968
/// An API for downloading and un-archiving artifacts, such as engine binaries or
/// additional source code.
class ArtifactUpdater {
  ArtifactUpdater({
969 970 971 972 973 974
    required OperatingSystemUtils operatingSystemUtils,
    required Logger logger,
    required FileSystem fileSystem,
    required Directory tempStorage,
    required HttpClient httpClient,
    required Platform platform,
975
    required List<String> allowedBaseUrls,
976
  }) : _operatingSystemUtils = operatingSystemUtils,
977
       _httpClient = httpClient,
978 979
       _logger = logger,
       _fileSystem = fileSystem,
980
       _tempStorage = tempStorage,
981 982
       _platform = platform,
       _allowedBaseUrls = allowedBaseUrls;
983 984 985

  /// The number of times the artifact updater will repeat the artifact download loop.
  static const int _kRetryCount = 2;
986 987 988 989 990

  final Logger _logger;
  final OperatingSystemUtils _operatingSystemUtils;
  final FileSystem _fileSystem;
  final Directory _tempStorage;
991 992
  final HttpClient _httpClient;
  final Platform _platform;
993

994 995 996 997 998 999 1000
  /// Artifacts should only be downloaded from URLs that use one of these
  /// prefixes.
  ///
  /// [ArtifactUpdater] will issue a warning if an attempt to download from a
  /// non-compliant URL is made.
  final List<String> _allowedBaseUrls;

1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040
  /// 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);
1041
    Status status;
1042
    int retries = _kRetryCount;
1043 1044

    while (retries > 0) {
1045 1046 1047
      status = _logger.startProgress(
        message,
      );
1048 1049
      try {
        _ensureExists(tempFile.parent);
1050 1051 1052
        if (tempFile.existsSync()) {
          tempFile.deleteSync();
        }
1053
        await _download(url, tempFile, status);
1054

1055 1056 1057
        if (!tempFile.existsSync()) {
          throw Exception('Did not find downloaded file ${tempFile.path}');
        }
1058 1059 1060 1061 1062
      } on Exception catch (err) {
        _logger.printTrace(err.toString());
        retries -= 1;
        if (retries == 0) {
          throwToolExit(
1063
            'Failed to download $url. Ensure you have network connectivity and then try again.\n$err',
1064 1065 1066 1067
          );
        }
        continue;
      } on ArgumentError catch (error) {
1068
        final String? overrideUrl = _platform.environment['FLUTTER_STORAGE_BASE_URL'];
1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081
        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;
1082 1083 1084
      } finally {
        status.stop();
      }
1085 1086 1087 1088 1089
      /// 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)
      );
1090 1091 1092 1093 1094 1095 1096 1097 1098 1099
      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;
1100
        if (_platform.isWindows && error.osError?.errorCode == kSharingViolation) {
1101 1102 1103 1104 1105 1106 1107
          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'
          );
        }
      }
1108 1109 1110 1111
      _ensureExists(location);

      try {
        extractor(tempFile, location);
1112
      } on Exception catch (err) {
1113 1114
        retries -= 1;
        if (retries == 0) {
1115 1116
          throwToolExit(
            'Flutter could not download and/or extract $url. Ensure you have '
1117
            'network connectivity and all of the required dependencies listed at '
1118 1119
            'flutter.dev/setup.\nThe original exception was: $err.'
          );
1120 1121 1122
        }
        _deleteIgnoringErrors(tempFile);
        continue;
1123 1124 1125 1126 1127
      }
      return;
    }
  }

1128
  /// Download bytes from [url], throwing non-200 responses as an exception.
1129 1130 1131 1132 1133 1134 1135
  ///
  /// 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
1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155
  Future<void> _download(Uri url, File file, Status status) async {
    final bool isAllowedUrl = _allowedBaseUrls.any((String baseUrl) => url.toString().startsWith(baseUrl));

    // In tests make this a hard failure.
    assert(
      isAllowedUrl,
      'URL not allowed: $url\n'
      'Allowed URLs must be based on one of: ${_allowedBaseUrls.join(', ')}',
    );

    // In production, issue a warning but allow the download to proceed.
    if (!isAllowedUrl) {
      status.pause();
      _logger.printWarning(
        'Downloading an artifact that may not be reachable in some environments (e.g. firewalled environments): $url\n'
        'This should not have happened. This is likely a Flutter SDK bug. Please file an issue at https://github.com/flutter/flutter/issues/new?template=1_activation.md'
      );
      status.resume();
    }

1156 1157 1158 1159 1160
    final HttpClientRequest request = await _httpClient.getUrl(url);
    final HttpClientResponse response = await request.close();
    if (response.statusCode != HttpStatus.ok) {
      throw Exception(response.statusCode);
    }
1161

1162 1163 1164
    final String? md5Hash = _expectedMd5(response.headers);
    ByteConversionSink? inputSink;
    late StreamController<Digest> digests;
1165 1166 1167 1168 1169 1170
    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);
1171
    await response.forEach((List<int> chunk) {
1172 1173
      inputSink?.add(chunk);
      randomAccessFile.writeFromSync(chunk);
1174
    });
1175 1176 1177 1178 1179 1180
    randomAccessFile.closeSync();
    if (inputSink != null) {
      inputSink.close();
      final Digest digest = await digests.stream.last;
      final String rawDigest = base64.encode(digest.bytes);
      if (rawDigest != md5Hash) {
1181
        throw Exception(
1182 1183 1184 1185 1186 1187 1188 1189 1190
          '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.'
        );
      }
    }
  }

1191 1192
  String? _expectedMd5(HttpHeaders httpHeaders) {
    final List<String>? values = httpHeaders['x-goog-hash'];
1193 1194 1195
    if (values == null) {
      return null;
    }
1196 1197 1198 1199 1200 1201 1202
    String? rawMd5Hash;
    for (final String value in values) {
      if (value.startsWith('md5=')) {
        rawMd5Hash = value;
        break;
      }
    }
1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214
    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;
1215 1216
  }

1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231
  /// 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);
    }
  }

1232
  /// Clear any zip/gzip files downloaded.
1233 1234 1235 1236 1237 1238 1239 1240
  void removeDownloadedFiles() {
    for (final File file in downloadedFiles) {
      if (!file.existsSync()) {
        continue;
      }
      try {
        file.deleteSync();
      } on FileSystemException catch (e) {
1241
        _logger.printWarning('Failed to delete "${file.path}". Please delete manually. $e');
1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265
        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
1266
String flattenNameSubdirs(Uri url, FileSystem fileSystem) {
1267 1268 1269 1270
  final List<String> pieces = <String>[url.host, ...url.pathSegments];
  final Iterable<String> convertedPieces = pieces.map<String>(_flattenNameNoSubdirs);
  return fileSystem.path.joinAll(convertedPieces);
}
1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295

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