cache.dart 24.2 KB
Newer Older
1 2 3 4 5 6
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';

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

9
import 'base/context.dart';
10
import 'base/file_system.dart';
11
import 'base/io.dart' show SocketException;
12
import 'base/logger.dart';
13
import 'base/net.dart';
14
import 'base/os.dart';
15
import 'base/platform.dart';
16 17
import 'globals.dart';

18
/// A wrapper around the `bin/cache/` directory.
19
class Cache {
20
  /// [rootOverride] is configurable for testing.
21 22 23
  /// [artifacts] is configurable for testing.
  Cache({ Directory rootOverride, List<CachedArtifact> artifacts }) : _rootOverride = rootOverride {
    if (artifacts == null) {
24 25 26
      _artifacts.add(MaterialFonts(this));
      _artifacts.add(FlutterEngine(this));
      _artifacts.add(GradleWrapper(this));
27 28 29 30
    } else {
      _artifacts.addAll(artifacts);
    }
  }
31

32
  static const List<String> _hostsBlockedInChina = <String> [
33 34 35
    'storage.googleapis.com',
  ];

36
  final Directory _rootOverride;
37
  final List<CachedArtifact> _artifacts = <CachedArtifact>[];
38

39 40 41
  // Initialized by FlutterCommandRunner on startup.
  static String flutterRoot;

42 43 44 45
  // Whether to cache artifacts for all platforms. Defaults to only caching
  // artifacts for the current platform.
  bool includeAllPlatforms = false;

46 47 48 49 50 51 52
  static RandomAccessFile _lock;
  static bool _lockEnabled = true;

  /// Turn off the [lock]/[releaseLockEarly] mechanism.
  ///
  /// 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.
53
  @visibleForTesting
54 55 56 57
  static void disableLocking() {
    _lockEnabled = false;
  }

58 59 60 61 62 63 64 65
  /// Turn on the [lock]/[releaseLockEarly] mechanism.
  ///
  /// This is used by the tests.
  @visibleForTesting
  static void enableLocking() {
    _lockEnabled = true;
  }

66 67 68 69 70 71 72
  /// Lock the cache directory.
  ///
  /// This happens automatically on startup (see [FlutterCommandRunner.runCommand]).
  ///
  /// Normally the lock will be held until the process exits (this uses normal
  /// POSIX flock semantics). Long-lived commands should release the lock by
  /// calling [Cache.releaseLockEarly] once they are no longer touching the cache.
73
  static Future<void> lock() async {
74
    if (!_lockEnabled)
75
      return;
76
    assert(_lock == null);
77
    _lock = await fs.file(fs.path.join(flutterRoot, 'bin', 'cache', 'lockfile')).open(mode: FileMode.write);
78 79 80 81 82 83 84 85
    bool locked = false;
    bool printed = false;
    while (!locked) {
      try {
        await _lock.lock();
        locked = true;
      } on FileSystemException {
        if (!printed) {
86 87
          printTrace('Waiting to be able to obtain lock of Flutter binary artifacts directory: ${_lock.path}');
          printStatus('Waiting for another flutter command to release the startup lock...');
88 89
          printed = true;
        }
90
        await Future<void>.delayed(const Duration(milliseconds: 50));
91 92 93 94 95 96
      }
    }
  }

  /// Releases the lock. This is not necessary unless the process is long-lived.
  static void releaseLockEarly() {
97
    if (!_lockEnabled || _lock == null)
98 99 100 101 102
      return;
    _lock.closeSync();
    _lock = null;
  }

103 104 105
  /// Checks if the current process owns the lock for the cache directory at
  /// this very moment; throws a [StateError] if it doesn't.
  static void checkLockAcquired() {
106
    if (_lockEnabled && _lock == null && platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') {
107
      throw StateError(
108 109 110 111 112
        'The current process does not own the lock for the cache directory. This is a bug in Flutter CLI tools.',
      );
    }
  }

113
  String _dartSdkVersion;
114

115 116 117 118 119 120 121 122 123 124 125 126
  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)'
      final String justVersion = platform.version.split(' ')[0];
      _dartSdkVersion = justVersion.replaceFirstMapped(RegExp(r'(\d+\.\d+\.\d+)(.+)'), (Match match) {
        final String noFlutter = match[2].replaceAll('.flutter-', ' ');
        return '${match[1]} (build ${match[1]}$noFlutter)';
      });
    }
    return _dartSdkVersion;
  }
127

128
  String _engineRevision;
129

130 131
  String get engineRevision {
    _engineRevision ??= getVersionFor('engine');
132 133 134
    return _engineRevision;
  }

135
  static Cache get instance => context[Cache];
136 137

  /// Return the top-level directory in the cache; this is `bin/cache`.
138 139
  Directory getRoot() {
    if (_rootOverride != null)
140
      return fs.directory(fs.path.join(_rootOverride.path, 'bin', 'cache'));
141
    else
142
      return fs.directory(fs.path.join(flutterRoot, 'bin', 'cache'));
143
  }
144

145 146
  /// Return a directory in the cache dir. For `pkg`, this will return `bin/cache/pkg`.
  Directory getCacheDir(String name) {
147
    final Directory dir = fs.directory(fs.path.join(getRoot().path, name));
148
    if (!dir.existsSync())
149
      dir.createSync(recursive: true);
150
    return dir;
151 152
  }

153 154 155
  /// Return the top-level directory for artifact downloads.
  Directory getDownloadDir() => getCacheDir('downloads');

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

159 160 161
  /// 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) {
162
    return getCacheArtifacts().childDirectory(name);
163 164
  }

165
  String getVersionFor(String artifactName) {
166
    final File versionFile = fs.file(fs.path.join(_rootOverride?.path ?? flutterRoot, 'bin', 'internal', '$artifactName.version'));
167 168 169
    return versionFile.existsSync() ? versionFile.readAsStringSync().trim() : null;
  }

170
  String getStampFor(String artifactName) {
171
    final File stampFile = getStampFileFor(artifactName);
172 173 174
    return stampFile.existsSync() ? stampFile.readAsStringSync().trim() : null;
  }

175 176 177 178 179
  void setStampFor(String artifactName, String version) {
    getStampFileFor(artifactName).writeAsStringSync(version);
  }

  File getStampFileFor(String artifactName) {
180
    return fs.file(fs.path.join(getRoot().path, '$artifactName.stamp'));
181 182
  }

183 184 185
  /// Returns `true` if either [entity] is older than the tools stamp or if
  /// [entity] doesn't exist.
  bool isOlderThanToolsStamp(FileSystemEntity entity) {
186
    final File flutterToolsStamp = getStampFileFor('flutter_tools');
187
    return isOlderThanReference(entity: entity, referenceFile: flutterToolsStamp);
188 189
  }

190
  bool isUpToDate() => _artifacts.every((CachedArtifact artifact) => artifact.isUpToDate());
191

192
  Future<String> getThirdPartyFile(String urlStr, String serviceName) async {
193 194
    final Uri url = Uri.parse(urlStr);
    final Directory thirdPartyDir = getArtifactDirectory('third_party');
195

196
    final Directory serviceDir = fs.directory(fs.path.join(thirdPartyDir.path, serviceName));
197
    if (!serviceDir.existsSync())
198 199
      serviceDir.createSync(recursive: true);

200
    final File cachedFile = fs.file(fs.path.join(serviceDir.path, url.pathSegments.last));
201 202
    if (!cachedFile.existsSync()) {
      try {
203
        await _downloadFile(url, cachedFile);
204 205
      } catch (e) {
        printError('Failed to fetch third-party artifact $url: $e');
206
        rethrow;
207 208 209 210 211 212
      }
    }

    return cachedFile.path;
  }

213
  Future<void> updateAll() async {
214
    if (!_lockEnabled)
215
      return;
216 217 218 219 220 221 222 223
    try {
      for (CachedArtifact artifact in _artifacts) {
        if (!artifact.isUpToDate())
          await artifact.update();
      }
    } on SocketException catch (e) {
      if (_hostsBlockedInChina.contains(e.address?.host)) {
        printError(
224 225 226
          'Failed to retrieve Flutter tool dependencies: ${e.message}.\n'
          'If you\'re in China, please see this page: '
          'https://flutter.io/community/china',
227 228 229 230
          emphasis: true,
        );
      }
      rethrow;
231 232 233 234
    }
  }
}

235 236 237
/// An artifact managed by the cache.
abstract class CachedArtifact {
  CachedArtifact(this.name, this.cache);
238

239
  final String name;
240 241
  final Cache cache;

242 243 244
  Directory get location => cache.getArtifactDirectory(name);
  String get version => cache.getVersionFor(name);

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

251
  bool isUpToDate() {
252 253 254
    if (!location.existsSync())
      return false;
    if (version != cache.getStampFor(name))
255
      return false;
256
    return isUpToDateInner();
257 258
  }

259
  Future<void> update() async {
260 261 262
    if (location.existsSync())
      location.deleteSync(recursive: true);
    location.createSync(recursive: true);
263 264
    await updateInner();
    cache.setStampFor(name, version);
265 266 267 268 269 270 271 272 273 274 275 276 277 278 279
    _removeDownloadedFiles();
  }

  /// Clear any zip/gzip files downloaded.
  void _removeDownloadedFiles() {
    for (File f in _downloadedFiles) {
      f.deleteSync();
      for (Directory d = f.parent; d.absolute.path != cache.getDownloadDir().absolute.path; d = d.parent) {
        if (d.listSync().isEmpty) {
          d.deleteSync();
        } else {
          break;
        }
      }
    }
280 281 282 283
  }

  /// Hook method for extra checks for being up-to-date.
  bool isUpToDateInner() => true;
284

285
  /// Template method to perform artifact update.
286
  Future<void> updateInner();
287 288 289 290 291 292 293 294 295 296

  String get _storageBaseUrl {
    final String overrideUrl = platform.environment['FLUTTER_STORAGE_BASE_URL'];
    if (overrideUrl == null)
      return 'https://storage.googleapis.com';
    _maybeWarnAboutStorageOverride(overrideUrl);
    return overrideUrl;
  }

  Uri _toStorageUri(String path) => Uri.parse('$_storageBaseUrl/$path');
297 298

  /// Download an archive from the given [url] and unzip it to [location].
299
  Future<void> _downloadArchive(String message, Uri url, Directory location, bool verifier(File f), void extractor(File f, Directory d)) {
300
    return _withDownloadFile('${flattenNameSubdirs(url)}', (File tempFile) async {
301
      if (!verifier(tempFile)) {
302
        final Status status = logger.startProgress(message, timeout: kSlowOperation);
303 304
        try {
          await _downloadFile(url, tempFile);
305
          status.stop();
306 307 308 309
        } catch (exception) {
          status.cancel();
          rethrow;
        }
310
      } else {
311
        logger.printTrace('$message (cached)');
312 313 314 315 316 317 318
      }
      _ensureExists(location);
      extractor(tempFile, location);
    });
  }

  /// Download a zip archive from the given [url] and unzip it to [location].
319
  Future<void> _downloadZipArchive(String message, Uri url, Directory location) {
320 321 322 323
    return _downloadArchive(message, url, location, os.verifyZip, os.unzip);
  }

  /// Download a gzipped tarball from the given [url] and unpack it to [location].
324
  Future<void> _downloadZippedTarball(String message, Uri url, Directory location) {
325 326 327 328 329
    return _downloadArchive(message, url, location, os.verifyGzip, os.unpack);
  }

  /// Create a temporary file and invoke [onTemporaryFile] with the file as
  /// argument, then add the temporary file to the [_downloadedFiles].
330
  Future<void> _withDownloadFile(String name, Future<void> onTemporaryFile(File file)) async {
331 332 333 334
    final File tempFile = fs.file(fs.path.join(cache.getDownloadDir().path, name));
    _downloadedFiles.add(tempFile);
    await onTemporaryFile(tempFile);
  }
335 336 337 338 339 340 341 342 343 344 345 346
}

bool _hasWarnedAboutStorageOverride = false;

void _maybeWarnAboutStorageOverride(String overrideUrl) {
  if (_hasWarnedAboutStorageOverride)
    return;
  logger.printStatus(
    'Flutter assets will be downloaded from $overrideUrl. Make sure you trust this source!',
    emphasis: true,
  );
  _hasWarnedAboutStorageOverride = true;
347
}
348

349 350
/// A cached artifact containing fonts used for Material Design.
class MaterialFonts extends CachedArtifact {
351
  MaterialFonts(Cache cache) : super('material_fonts', cache);
352 353

  @override
354
  Future<void> updateInner() {
355
    final Uri archiveUri = _toStorageUri(version);
356
    return _downloadZipArchive('Downloading Material fonts...', archiveUri, location);
357 358
  }
}
359

360 361
/// A cached artifact containing the Flutter engine binaries.
class FlutterEngine extends CachedArtifact {
362
  FlutterEngine(Cache cache) : super('engine', cache);
363

364
  List<String> _getPackageDirs() => const <String>['sky_engine'];
Devon Carew's avatar
Devon Carew committed
365

366 367
  // Return a list of (cache directory path, download URL path) tuples.
  List<List<String>> _getBinaryDirs() {
368
    final List<List<String>> binaryDirs = <List<String>>[];
369

370 371
    binaryDirs.add(<String>['common', 'flutter_patched_sdk.zip']);

372
    if (cache.includeAllPlatforms)
373 374 375
      binaryDirs
        ..addAll(_osxBinaryDirs)
        ..addAll(_linuxBinaryDirs)
376
        ..addAll(_windowsBinaryDirs)
377
        ..addAll(_androidBinaryDirs)
378 379
        ..addAll(_iosBinaryDirs)
        ..addAll(_dartSdks);
380
    else if (platform.isLinux)
381 382 383
      binaryDirs
        ..addAll(_linuxBinaryDirs)
        ..addAll(_androidBinaryDirs);
384
    else if (platform.isMacOS)
385 386 387 388
      binaryDirs
        ..addAll(_osxBinaryDirs)
        ..addAll(_androidBinaryDirs)
        ..addAll(_iosBinaryDirs);
389 390 391 392
    else if (platform.isWindows)
      binaryDirs
        ..addAll(_windowsBinaryDirs)
        ..addAll(_androidBinaryDirs);
393 394

    return binaryDirs;
Devon Carew's avatar
Devon Carew committed
395
  }
396

397
  List<List<String>> get _osxBinaryDirs => <List<String>>[
398 399 400
    <String>['darwin-x64', 'darwin-x64/artifacts.zip'],
    <String>['android-arm-profile/darwin-x64', 'android-arm-profile/darwin-x64.zip'],
    <String>['android-arm-release/darwin-x64', 'android-arm-release/darwin-x64.zip'],
401 402
    <String>['android-arm64-profile/darwin-x64', 'android-arm64-profile/darwin-x64.zip'],
    <String>['android-arm64-release/darwin-x64', 'android-arm64-release/darwin-x64.zip'],
403 404 405 406
    <String>['android-arm-dynamic-profile/darwin-x64', 'android-arm-dynamic-profile/darwin-x64.zip'],
    <String>['android-arm-dynamic-release/darwin-x64', 'android-arm-dynamic-release/darwin-x64.zip'],
    <String>['android-arm64-dynamic-profile/darwin-x64', 'android-arm64-dynamic-profile/darwin-x64.zip'],
    <String>['android-arm64-dynamic-release/darwin-x64', 'android-arm64-dynamic-release/darwin-x64.zip'],
407 408
  ];

409
  List<List<String>> get _linuxBinaryDirs => <List<String>>[
410 411 412
    <String>['linux-x64', 'linux-x64/artifacts.zip'],
    <String>['android-arm-profile/linux-x64', 'android-arm-profile/linux-x64.zip'],
    <String>['android-arm-release/linux-x64', 'android-arm-release/linux-x64.zip'],
413 414
    <String>['android-arm64-profile/linux-x64', 'android-arm64-profile/linux-x64.zip'],
    <String>['android-arm64-release/linux-x64', 'android-arm64-release/linux-x64.zip'],
415 416 417 418
    <String>['android-arm-dynamic-profile/linux-x64', 'android-arm-dynamic-profile/linux-x64.zip'],
    <String>['android-arm-dynamic-release/linux-x64', 'android-arm-dynamic-release/linux-x64.zip'],
    <String>['android-arm64-dynamic-profile/linux-x64', 'android-arm64-dynamic-profile/linux-x64.zip'],
    <String>['android-arm64-dynamic-release/linux-x64', 'android-arm64-dynamic-release/linux-x64.zip'],
419 420
  ];

421
  List<List<String>> get _windowsBinaryDirs => <List<String>>[
422
    <String>['windows-x64', 'windows-x64/artifacts.zip'],
423 424
    <String>['android-arm-profile/windows-x64', 'android-arm-profile/windows-x64.zip'],
    <String>['android-arm-release/windows-x64', 'android-arm-release/windows-x64.zip'],
425 426
    <String>['android-arm64-profile/windows-x64', 'android-arm64-profile/windows-x64.zip'],
    <String>['android-arm64-release/windows-x64', 'android-arm64-release/windows-x64.zip'],
427 428 429 430
    <String>['android-arm-dynamic-profile/windows-x64', 'android-arm-dynamic-profile/windows-x64.zip'],
    <String>['android-arm-dynamic-release/windows-x64', 'android-arm-dynamic-release/windows-x64.zip'],
    <String>['android-arm64-dynamic-profile/windows-x64', 'android-arm64-dynamic-profile/windows-x64.zip'],
    <String>['android-arm64-dynamic-release/windows-x64', 'android-arm64-dynamic-release/windows-x64.zip'],
431 432
  ];

433 434 435 436 437 438
  List<List<String>> get _androidBinaryDirs => <List<String>>[
    <String>['android-x86', 'android-x86/artifacts.zip'],
    <String>['android-x64', 'android-x64/artifacts.zip'],
    <String>['android-arm', 'android-arm/artifacts.zip'],
    <String>['android-arm-profile', 'android-arm-profile/artifacts.zip'],
    <String>['android-arm-release', 'android-arm-release/artifacts.zip'],
439 440 441
    <String>['android-arm64', 'android-arm64/artifacts.zip'],
    <String>['android-arm64-profile', 'android-arm64-profile/artifacts.zip'],
    <String>['android-arm64-release', 'android-arm64-release/artifacts.zip'],
442 443 444 445
    <String>['android-arm-dynamic-profile', 'android-arm-dynamic-profile/artifacts.zip'],
    <String>['android-arm-dynamic-release', 'android-arm-dynamic-release/artifacts.zip'],
    <String>['android-arm64-dynamic-profile', 'android-arm64-dynamic-profile/artifacts.zip'],
    <String>['android-arm64-dynamic-release', 'android-arm64-dynamic-release/artifacts.zip'],
446 447 448 449 450 451 452 453
  ];

  List<List<String>> get _iosBinaryDirs => <List<String>>[
    <String>['ios', 'ios/artifacts.zip'],
    <String>['ios-profile', 'ios-profile/artifacts.zip'],
    <String>['ios-release', 'ios-release/artifacts.zip'],
  ];

454 455 456 457 458 459
  List<List<String>> get _dartSdks => <List<String>> [
    <String>['darwin-x64', 'dart-sdk-darwin-x64.zip'],
    <String>['linux-x64', 'dart-sdk-linux-x64.zip'],
    <String>['windows-x64', 'dart-sdk-windows-x64.zip'],
  ];

460 461 462 463 464 465 466 467
  // A list of cache directory paths to which the LICENSE file should be copied.
  List<String> _getLicenseDirs() {
    if (cache.includeAllPlatforms || platform.isMacOS) {
      return const <String>['ios', 'ios-profile', 'ios-release'];
    }
    return const <String>[];
  }

468 469
  @override
  bool isUpToDateInner() {
470
    final Directory pkgDir = cache.getCacheDir('pkg');
471
    for (String pkgName in _getPackageDirs()) {
472
      final String pkgPath = fs.path.join(pkgDir.path, pkgName);
473
      if (!fs.directory(pkgPath).existsSync())
474
        return false;
475 476
    }

477
    for (List<String> toolsDir in _getBinaryDirs()) {
478
      final Directory dir = fs.directory(fs.path.join(location.path, toolsDir[0]));
Devon Carew's avatar
Devon Carew committed
479 480 481
      if (!dir.existsSync())
        return false;
    }
482 483 484 485 486 487

    for (String licenseDir in _getLicenseDirs()) {
      final File file = fs.file(fs.path.join(location.path, licenseDir, 'LICENSE'));
      if (!file.existsSync())
        return false;
    }
488
    return true;
489 490
  }

491
  @override
492
  Future<void> updateInner() async {
493
    final String url = '$_storageBaseUrl/flutter_infra/flutter/$version/';
494

495
    final Directory pkgDir = cache.getCacheDir('pkg');
496
    for (String pkgName in _getPackageDirs()) {
497 498
      final String pkgPath = fs.path.join(pkgDir.path, pkgName);
      final Directory dir = fs.directory(pkgPath);
499 500
      if (dir.existsSync())
        dir.deleteSync(recursive: true);
501
      await _downloadZipArchive('Downloading package $pkgName...', Uri.parse(url + pkgName + '.zip'), pkgDir);
502 503
    }

504
    for (List<String> toolsDir in _getBinaryDirs()) {
505 506
      final String cacheDir = toolsDir[0];
      final String urlPath = toolsDir[1];
507
      final Directory dir = fs.directory(fs.path.join(location.path, cacheDir));
508
      await _downloadZipArchive('Downloading $cacheDir tools...', Uri.parse(url + urlPath), dir);
509 510 511

      _makeFilesExecutable(dir);

512
      final File frameworkZip = fs.file(fs.path.join(dir.path, 'Flutter.framework.zip'));
513
      if (frameworkZip.existsSync()) {
514
        final Directory framework = fs.directory(fs.path.join(dir.path, 'Flutter.framework'));
515 516
        framework.createSync();
        os.unzip(frameworkZip, framework);
Devon Carew's avatar
Devon Carew committed
517 518
      }
    }
519 520 521 522 523 524

    final File licenseSource = fs.file(fs.path.join(Cache.flutterRoot, 'LICENSE'));
    for (String licenseDir in _getLicenseDirs()) {
      final String licenseDestinationPath = fs.path.join(location.path, licenseDir, 'LICENSE');
      await licenseSource.copy(licenseDestinationPath);
    }
525 526
  }

527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562
  Future<bool> areRemoteArtifactsAvailable({String engineVersion,
                                            bool includeAllPlatforms = true}) async {
    final bool includeAllPlatformsState = cache.includeAllPlatforms;
    cache.includeAllPlatforms = includeAllPlatforms;

    Future<bool> checkForArtifacts(String engineVersion) async {
      engineVersion ??= version;
      final String url = '$_storageBaseUrl/flutter_infra/flutter/$engineVersion/';

      bool exists = false;
      for (String pkgName in _getPackageDirs()) {
        exists = await _doesRemoteExist('Checking package $pkgName is available...',
            Uri.parse(url + pkgName + '.zip'));
        if (!exists) {
          return false;
        }
      }

      for (List<String> toolsDir in _getBinaryDirs()) {
        final String cacheDir = toolsDir[0];
        final String urlPath = toolsDir[1];
        exists = await _doesRemoteExist('Checking $cacheDir tools are available...',
            Uri.parse(url + urlPath));
        if (!exists) {
          return false;
        }
      }

      return true;
    }

    final bool result = await checkForArtifacts(engineVersion);
    cache.includeAllPlatforms = includeAllPlatformsState;
    return result;
  }

563

564
  void _makeFilesExecutable(Directory dir) {
565
    for (FileSystemEntity entity in dir.listSync()) {
566
      if (entity is File) {
567 568 569
        final String name = fs.path.basename(entity.path);
        if (name == 'flutter_tester')
          os.makeExecutable(entity);
570 571 572
      }
    }
  }
573 574 575 576
}

/// A cached artifact containing Gradle Wrapper scripts and binaries.
class GradleWrapper extends CachedArtifact {
577
  GradleWrapper(Cache cache) : super('gradle_wrapper', cache);
578

579 580 581 582
  List<String> get _gradleScripts => <String>['gradlew', 'gradlew.bat'];

  String get _gradleWrapper => fs.path.join('gradle', 'wrapper', 'gradle-wrapper.jar');

583
  @override
584
  Future<void> updateInner() {
585
    final Uri archiveUri = _toStorageUri(version);
586
    return _downloadZippedTarball('Downloading Gradle Wrapper...', archiveUri, location).then<void>((_) {
587 588
      // Delete property file, allowing templates to provide it.
      fs.file(fs.path.join(location.path, 'gradle', 'wrapper', 'gradle-wrapper.properties')).deleteSync();
589 590
      // Remove NOTICE file. Should not be part of the template.
      fs.file(fs.path.join(location.path, 'NOTICE')).deleteSync();
591
    });
592
  }
593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608

  @override
  bool isUpToDateInner() {
    final Directory wrapperDir = cache.getCacheDir(fs.path.join('artifacts', 'gradle_wrapper'));
    if (!fs.directory(wrapperDir).existsSync())
      return false;
    for (String scriptName in _gradleScripts) {
      final File scriptFile = fs.file(fs.path.join(wrapperDir.path, scriptName));
      if (!scriptFile.existsSync())
        return false;
    }
    final File gradleWrapperJar = fs.file(fs.path.join(wrapperDir.path, _gradleWrapper));
    if (!gradleWrapperJar.existsSync())
      return false;
    return true;
  }
609
}
610

611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632
// 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
};

/// 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) {
    replacedCodeUnits.addAll(_flattenNameSubstitutions[codeUnit] ?? <int>[codeUnit]);
  }
633
  return String.fromCharCodes(replacedCodeUnits);
634 635
}

636 637
@visibleForTesting
String flattenNameSubdirs(Uri url) {
638
  final List<String> pieces = <String>[url.host]..addAll(url.pathSegments);
639
  final Iterable<String> convertedPieces = pieces.map<String>(_flattenNameNoSubdirs);
640 641 642
  return fs.path.joinAll(convertedPieces);
}

643
/// Download a file from the given [url] and write it to [location].
644
Future<void> _downloadFile(Uri url, File location) async {
645 646 647 648 649
  _ensureExists(location.parent);
  final List<int> fileBytes = await fetchUrl(url);
  location.writeAsBytesSync(fileBytes, flush: true);
}

650
Future<bool> _doesRemoteExist(String message, Uri url) async {
651
  final Status status = logger.startProgress(message, timeout: kSlowOperation);
652 653 654 655 656
  final bool exists = await doesRemoteFileExist(url);
  status.stop();
  return exists;
}

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