cache.dart 15.1 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/logger.dart';
12
import 'base/net.dart';
13
import 'base/os.dart';
14
import 'base/platform.dart';
15 16
import 'globals.dart';

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

31
  final Directory _rootOverride;
32
  final List<CachedArtifact> _artifacts = <CachedArtifact>[];
33

34 35 36
  // Initialized by FlutterCommandRunner on startup.
  static String flutterRoot;

37 38 39 40
  // Whether to cache artifacts for all platforms. Defaults to only caching
  // artifacts for the current platform.
  bool includeAllPlatforms = false;

41 42 43 44 45 46 47
  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.
48
  @visibleForTesting
49 50 51 52
  static void disableLocking() {
    _lockEnabled = false;
  }

53 54 55 56 57 58 59 60
  /// Turn on the [lock]/[releaseLockEarly] mechanism.
  ///
  /// This is used by the tests.
  @visibleForTesting
  static void enableLocking() {
    _lockEnabled = true;
  }

61 62 63 64 65 66 67 68 69 70 71
  /// 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.
  static Future<Null> lock() async {
    if (!_lockEnabled)
      return null;
    assert(_lock == null);
72
    _lock = await fs.file(fs.path.join(flutterRoot, 'bin', 'cache', 'lockfile')).open(mode: FileMode.WRITE);
73 74 75 76 77 78 79 80
    bool locked = false;
    bool printed = false;
    while (!locked) {
      try {
        await _lock.lock();
        locked = true;
      } on FileSystemException {
        if (!printed) {
81 82
          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...');
83 84
          printed = true;
        }
85
        await new Future<Null>.delayed(const Duration(milliseconds: 50));
86 87 88 89 90 91
      }
    }
  }

  /// Releases the lock. This is not necessary unless the process is long-lived.
  static void releaseLockEarly() {
92
    if (!_lockEnabled || _lock == null)
93 94 95 96 97
      return;
    _lock.closeSync();
    _lock = null;
  }

98 99 100
  /// 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() {
101
    if (_lockEnabled && _lock == null && platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') {
102 103 104 105 106 107
      throw new StateError(
        'The current process does not own the lock for the cache directory. This is a bug in Flutter CLI tools.',
      );
    }
  }

108 109
  static String _dartSdkVersion;

110
  static String get dartSdkVersion => _dartSdkVersion ??= platform.version;
111

112 113 114 115
  static String _engineRevision;

  static String get engineRevision {
    if (_engineRevision == null) {
116
      final File revisionFile = fs.file(fs.path.join(flutterRoot, 'bin', 'internal', 'engine.version'));
117 118 119 120 121 122
      if (revisionFile.existsSync())
        _engineRevision = revisionFile.readAsStringSync().trim();
    }
    return _engineRevision;
  }

123
  static Cache get instance => context[Cache];
124 125

  /// Return the top-level directory in the cache; this is `bin/cache`.
126 127
  Directory getRoot() {
    if (_rootOverride != null)
128
      return fs.directory(fs.path.join(_rootOverride.path, 'bin', 'cache'));
129
    else
130
      return fs.directory(fs.path.join(flutterRoot, 'bin', 'cache'));
131
  }
132

133 134
  /// Return a directory in the cache dir. For `pkg`, this will return `bin/cache/pkg`.
  Directory getCacheDir(String name) {
135
    final Directory dir = fs.directory(fs.path.join(getRoot().path, name));
136
    if (!dir.existsSync())
137
      dir.createSync(recursive: true);
138
    return dir;
139 140
  }

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

144 145 146
  /// 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) {
147
    return fs.directory(fs.path.join(getCacheArtifacts().path, name));
148 149
  }

150
  String getVersionFor(String artifactName) {
151
    final File versionFile = fs.file(fs.path.join(_rootOverride?.path ?? flutterRoot, 'bin', 'internal', '$artifactName.version'));
152 153 154
    return versionFile.existsSync() ? versionFile.readAsStringSync().trim() : null;
  }

155
  String getStampFor(String artifactName) {
156
    final File stampFile = getStampFileFor(artifactName);
157 158 159
    return stampFile.existsSync() ? stampFile.readAsStringSync().trim() : null;
  }

160 161 162 163 164
  void setStampFor(String artifactName, String version) {
    getStampFileFor(artifactName).writeAsStringSync(version);
  }

  File getStampFileFor(String artifactName) {
165
    return fs.file(fs.path.join(getRoot().path, '$artifactName.stamp'));
166 167
  }

168
  bool isUpToDate() => _artifacts.every((CachedArtifact artifact) => artifact.isUpToDate());
169

170
  Future<String> getThirdPartyFile(String urlStr, String serviceName) async {
171 172
    final Uri url = Uri.parse(urlStr);
    final Directory thirdPartyDir = getArtifactDirectory('third_party');
173

174
    final Directory serviceDir = fs.directory(fs.path.join(thirdPartyDir.path, serviceName));
175 176 177
    if (!serviceDir.existsSync())
      serviceDir.createSync(recursive: true);

178
    final File cachedFile = fs.file(fs.path.join(serviceDir.path, url.pathSegments.last));
179 180
    if (!cachedFile.existsSync()) {
      try {
181
        await _downloadFile(url, cachedFile);
182 183
      } catch (e) {
        printError('Failed to fetch third-party artifact $url: $e');
184
        rethrow;
185 186 187 188 189 190
      }
    }

    return cachedFile.path;
  }

191
  Future<Null> updateAll() async {
192 193
    if (!_lockEnabled)
      return null;
194 195 196
    for (CachedArtifact artifact in _artifacts) {
      if (!artifact.isUpToDate())
        await artifact.update();
197 198 199 200
    }
  }
}

201 202 203
/// An artifact managed by the cache.
abstract class CachedArtifact {
  CachedArtifact(this.name, this.cache);
204

205
  final String name;
206 207
  final Cache cache;

208 209 210
  Directory get location => cache.getArtifactDirectory(name);
  String get version => cache.getVersionFor(name);

211
  bool isUpToDate() {
212 213 214
    if (!location.existsSync())
      return false;
    if (version != cache.getStampFor(name))
215
      return false;
216
    return isUpToDateInner();
217 218
  }

219 220 221 222 223 224 225 226 227 228 229
  Future<Null> update() async {
    if (location.existsSync())
      location.deleteSync(recursive: true);
    location.createSync(recursive: true);
    return updateInner().then<Null>((_) {
      cache.setStampFor(name, version);
    });
  }

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

231 232 233
  /// Template method to perform artifact update.
  Future<Null> updateInner();
}
234

235 236 237 238 239 240 241 242
/// A cached artifact containing fonts used for Material Design.
class MaterialFonts extends CachedArtifact {
  MaterialFonts(Cache cache): super('material_fonts', cache);

  @override
  Future<Null> updateInner() {
    final Status status = logger.startProgress('Downloading Material fonts...', expectSlowOperation: true);
    return _downloadZipArchive(Uri.parse(version), location).then<Null>((_) {
Devon Carew's avatar
Devon Carew committed
243
      status.stop();
244
    }).whenComplete(status.cancel);
245 246
  }
}
247

248 249 250
/// A cached artifact containing the Flutter engine binaries.
class FlutterEngine extends CachedArtifact {
  FlutterEngine(Cache cache): super('engine', cache);
251

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

254 255
  // Return a list of (cache directory path, download URL path) tuples.
  List<List<String>> _getBinaryDirs() {
256
    final List<List<String>> binaryDirs = <List<String>>[];
257

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

260
    if (cache.includeAllPlatforms)
261 262 263
      binaryDirs
        ..addAll(_osxBinaryDirs)
        ..addAll(_linuxBinaryDirs)
264
        ..addAll(_windowsBinaryDirs)
265 266
        ..addAll(_androidBinaryDirs)
        ..addAll(_iosBinaryDirs);
267
    else if (platform.isLinux)
268 269 270
      binaryDirs
        ..addAll(_linuxBinaryDirs)
        ..addAll(_androidBinaryDirs);
271
    else if (platform.isMacOS)
272 273 274 275
      binaryDirs
        ..addAll(_osxBinaryDirs)
        ..addAll(_androidBinaryDirs)
        ..addAll(_iosBinaryDirs);
276 277 278 279
    else if (platform.isWindows)
      binaryDirs
        ..addAll(_windowsBinaryDirs)
        ..addAll(_androidBinaryDirs);
280 281

    return binaryDirs;
Devon Carew's avatar
Devon Carew committed
282
  }
283

284
  List<List<String>> get _osxBinaryDirs => <List<String>>[
285
    <String>['darwin-x64', 'darwin-x64/artifacts.zip'],
286
    <String>['darwin-x64', 'dart-sdk-darwin-x64.zip'],
287 288 289 290
    <String>['android-arm-profile/darwin-x64', 'android-arm-profile/darwin-x64.zip'],
    <String>['android-arm-release/darwin-x64', 'android-arm-release/darwin-x64.zip'],
  ];

291
  List<List<String>> get _linuxBinaryDirs => <List<String>>[
292
    <String>['linux-x64', 'linux-x64/artifacts.zip'],
293
    <String>['linux-x64', 'dart-sdk-linux-x64.zip'],
294 295 296 297
    <String>['android-arm-profile/linux-x64', 'android-arm-profile/linux-x64.zip'],
    <String>['android-arm-release/linux-x64', 'android-arm-release/linux-x64.zip'],
  ];

298
  List<List<String>> get _windowsBinaryDirs => <List<String>>[
299
    <String>['windows-x64', 'windows-x64/artifacts.zip'],
300
    <String>['windows-x64', 'dart-sdk-windows-x64.zip'],
301 302 303 304
    <String>['android-arm-profile/windows-x64', 'android-arm-profile/windows-x64.zip'],
    <String>['android-arm-release/windows-x64', 'android-arm-release/windows-x64.zip'],
  ];

305 306 307 308 309 310 311 312 313 314 315 316 317 318
  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'],
  ];

  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'],
  ];

319 320
  @override
  bool isUpToDateInner() {
321
    final Directory pkgDir = cache.getCacheDir('pkg');
322
    for (String pkgName in _getPackageDirs()) {
323
      final String pkgPath = fs.path.join(pkgDir.path, pkgName);
324
      if (!fs.directory(pkgPath).existsSync())
325
        return false;
326 327
    }

328
    for (List<String> toolsDir in _getBinaryDirs()) {
329
      final Directory dir = fs.directory(fs.path.join(location.path, toolsDir[0]));
Devon Carew's avatar
Devon Carew committed
330 331 332
      if (!dir.existsSync())
        return false;
    }
333
    return true;
334 335
  }

336 337 338
  @override
  Future<Null> updateInner() async {
    final String url = 'https://storage.googleapis.com/flutter_infra/flutter/$version/';
339

340
    final Directory pkgDir = cache.getCacheDir('pkg');
341
    for (String pkgName in _getPackageDirs()) {
342 343
      final String pkgPath = fs.path.join(pkgDir.path, pkgName);
      final Directory dir = fs.directory(pkgPath);
344 345 346
      if (dir.existsSync())
        dir.deleteSync(recursive: true);
      await _downloadItem('Downloading package $pkgName...', url + pkgName + '.zip', pkgDir);
347 348
    }

349
    for (List<String> toolsDir in _getBinaryDirs()) {
350 351
      final String cacheDir = toolsDir[0];
      final String urlPath = toolsDir[1];
352
      final Directory dir = fs.directory(fs.path.join(location.path, cacheDir));
353 354 355 356
      await _downloadItem('Downloading $cacheDir tools...', url + urlPath, dir);

      _makeFilesExecutable(dir);

357
      final File frameworkZip = fs.file(fs.path.join(dir.path, 'Flutter.framework.zip'));
358
      if (frameworkZip.existsSync()) {
359
        final Directory framework = fs.directory(fs.path.join(dir.path, 'Flutter.framework'));
360 361
        framework.createSync();
        os.unzip(frameworkZip, framework);
Devon Carew's avatar
Devon Carew committed
362 363
      }
    }
364 365 366 367 368
  }

  void _makeFilesExecutable(Directory dir) {
    for (FileSystemEntity entity in dir.listSync()) {
      if (entity is File) {
369
        final String name = fs.path.basename(entity.path);
370
        if (name == 'flutter_tester')
371 372 373 374 375 376
          os.makeExecutable(entity);
      }
    }
  }

  Future<Null> _downloadItem(String message, String url, Directory dest) {
377
    final Status status = logger.startProgress(message, expectSlowOperation: true);
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396
    return _downloadZipArchive(Uri.parse(url), dest).then<Null>((_) {
      status.stop();
    }).whenComplete(status.cancel);
  }
}

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

  @override
  Future<Null> updateInner() async {
    final Status status = logger.startProgress('Downloading Gradle Wrapper...', expectSlowOperation: true);

    final String url = 'https://android.googlesource.com'
        '/platform/tools/base/+archive/$version/templates/gradle/wrapper.tgz';
    await _downloadZippedTarball(Uri.parse(url), location).then<Null>((_) {
      // Delete property file, allowing templates to provide it.
      fs.file(fs.path.join(location.path, 'gradle', 'wrapper', 'gradle-wrapper.properties')).deleteSync();
Devon Carew's avatar
Devon Carew committed
397
      status.stop();
398
    }).whenComplete(status.cancel);
399 400
  }
}
401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442

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

/// Download a zip archive from the given [url] and unzip it to [location].
Future<Null> _downloadZipArchive(Uri url, Directory location) {
  return _withTemporaryFile('download.zip', (File tempFile) async {
    await _downloadFile(url, tempFile);
    _ensureExists(location);
    os.unzip(tempFile, location);
  });
}

/// Download a gzipped tarball from the given [url] and unpack it to [location].
Future<Null> _downloadZippedTarball(Uri url, Directory location) {
  return _withTemporaryFile('download.tgz', (File tempFile) async {
    await _downloadFile(url, tempFile);
    _ensureExists(location);
    os.unpack(tempFile, location);
  });
}

/// Create a file with the given name in a new temporary directory, invoke
/// [onTemporaryFile] with the file as argument, then delete the temporary
/// directory.
Future<Null> _withTemporaryFile(String name, Future<Null> onTemporaryFile(File file)) async {
  final Directory tempDir = fs.systemTempDirectory.createTempSync();
  final File tempFile = fs.file(fs.path.join(tempDir.path, name));
  await onTemporaryFile(tempFile).whenComplete(() {
    tempDir.delete(recursive: true);
  });
}

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