• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2016 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5import 'dart:async';
6
7import 'package:meta/meta.dart';
8
9import 'base/common.dart';
10import 'base/context.dart';
11import 'base/file_system.dart';
12import 'base/io.dart' show SocketException;
13import 'base/logger.dart';
14import 'base/net.dart';
15import 'base/os.dart';
16import 'base/platform.dart';
17import 'globals.dart';
18
19/// A tag for a set of development artifacts that need to be cached.
20class DevelopmentArtifact {
21
22  const DevelopmentArtifact._(this.name, {this.unstable = false});
23
24  /// The name of the artifact.
25  ///
26  /// This should match the flag name in precache.dart
27  final String name;
28
29  /// Whether this artifact should be unavailable on stable branches.
30  final bool unstable;
31
32  /// Artifacts required for Android development.
33  static const DevelopmentArtifact android = DevelopmentArtifact._('android');
34
35  /// Artifacts required for iOS development.
36  static const DevelopmentArtifact iOS = DevelopmentArtifact._('ios');
37
38  /// Artifacts required for web development.
39  static const DevelopmentArtifact web = DevelopmentArtifact._('web', unstable: true);
40
41  /// Artifacts required for desktop macOS.
42  static const DevelopmentArtifact macOS = DevelopmentArtifact._('macos', unstable: true);
43
44  /// Artifacts required for desktop Windows.
45  static const DevelopmentArtifact windows = DevelopmentArtifact._('windows', unstable: true);
46
47  /// Artifacts required for desktop Linux.
48  static const DevelopmentArtifact linux = DevelopmentArtifact._('linux', unstable: true);
49
50  /// Artifacts required for Fuchsia.
51  static const DevelopmentArtifact fuchsia = DevelopmentArtifact._('fuchsia', unstable: true);
52
53  /// Artifacts required for the Flutter Runner.
54  static const DevelopmentArtifact flutterRunner = DevelopmentArtifact._('flutter_runner', unstable: true);
55
56  /// Artifacts required for any development platform.
57  static const DevelopmentArtifact universal = DevelopmentArtifact._('universal');
58
59  /// The values of DevelopmentArtifacts.
60  static final List<DevelopmentArtifact> values = <DevelopmentArtifact>[
61    android,
62    iOS,
63    web,
64    macOS,
65    windows,
66    linux,
67    fuchsia,
68    universal,
69    flutterRunner,
70  ];
71}
72
73/// A wrapper around the `bin/cache/` directory.
74class Cache {
75  /// [rootOverride] is configurable for testing.
76  /// [artifacts] is configurable for testing.
77  Cache({ Directory rootOverride, List<CachedArtifact> artifacts }) : _rootOverride = rootOverride {
78    if (artifacts == null) {
79      _artifacts.add(MaterialFonts(this));
80      _artifacts.add(AndroidEngineArtifacts(this));
81      _artifacts.add(IOSEngineArtifacts(this));
82      _artifacts.add(GradleWrapper(this));
83      _artifacts.add(FlutterWebSdk(this));
84      _artifacts.add(FlutterSdk(this));
85      _artifacts.add(WindowsEngineArtifacts(this));
86      _artifacts.add(MacOSEngineArtifacts(this));
87      _artifacts.add(LinuxEngineArtifacts(this));
88      _artifacts.add(LinuxFuchsiaSDKArtifacts(this));
89      _artifacts.add(MacOSFuchsiaSDKArtifacts(this));
90      _artifacts.add(FlutterRunnerSDKArtifacts(this));
91      for (String artifactName in IosUsbArtifacts.artifactNames) {
92        _artifacts.add(IosUsbArtifacts(artifactName, this));
93      }
94    } else {
95      _artifacts.addAll(artifacts);
96    }
97  }
98
99  static const List<String> _hostsBlockedInChina = <String> [
100    'storage.googleapis.com',
101  ];
102
103  final Directory _rootOverride;
104  final List<CachedArtifact> _artifacts = <CachedArtifact>[];
105
106  // Initialized by FlutterCommandRunner on startup.
107  static String flutterRoot;
108
109  // Whether to cache artifacts for all platforms. Defaults to only caching
110  // artifacts for the current platform.
111  bool includeAllPlatforms = false;
112
113  static RandomAccessFile _lock;
114  static bool _lockEnabled = true;
115
116  /// Turn off the [lock]/[releaseLockEarly] mechanism.
117  ///
118  /// This is used by the tests since they run simultaneously and all in one
119  /// process and so it would be a mess if they had to use the lock.
120  @visibleForTesting
121  static void disableLocking() {
122    _lockEnabled = false;
123  }
124
125  /// Turn on the [lock]/[releaseLockEarly] mechanism.
126  ///
127  /// This is used by the tests.
128  @visibleForTesting
129  static void enableLocking() {
130    _lockEnabled = true;
131  }
132
133  /// Lock the cache directory.
134  ///
135  /// This happens automatically on startup (see [FlutterCommandRunner.runCommand]).
136  ///
137  /// Normally the lock will be held until the process exits (this uses normal
138  /// POSIX flock semantics). Long-lived commands should release the lock by
139  /// calling [Cache.releaseLockEarly] once they are no longer touching the cache.
140  static Future<void> lock() async {
141    if (!_lockEnabled)
142      return;
143    assert(_lock == null);
144    final File lockFile =
145        fs.file(fs.path.join(flutterRoot, 'bin', 'cache', 'lockfile'));
146    try {
147      _lock = lockFile.openSync(mode: FileMode.write);
148    } on FileSystemException catch (e) {
149      printError('Failed to open or create the artifact cache lockfile: "$e"');
150      printError('Please ensure you have permissions to create or open '
151                 '${lockFile.path}');
152      throwToolExit('Failed to open or create the lockfile');
153    }
154    bool locked = false;
155    bool printed = false;
156    while (!locked) {
157      try {
158        _lock.lockSync();
159        locked = true;
160      } on FileSystemException {
161        if (!printed) {
162          printTrace('Waiting to be able to obtain lock of Flutter binary artifacts directory: ${_lock.path}');
163          printStatus('Waiting for another flutter command to release the startup lock...');
164          printed = true;
165        }
166        await Future<void>.delayed(const Duration(milliseconds: 50));
167      }
168    }
169  }
170
171  /// Releases the lock. This is not necessary unless the process is long-lived.
172  static void releaseLockEarly() {
173    if (!_lockEnabled || _lock == null)
174      return;
175    _lock.closeSync();
176    _lock = null;
177  }
178
179  /// Checks if the current process owns the lock for the cache directory at
180  /// this very moment; throws a [StateError] if it doesn't.
181  static void checkLockAcquired() {
182    if (_lockEnabled && _lock == null && platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') {
183      throw StateError(
184        'The current process does not own the lock for the cache directory. This is a bug in Flutter CLI tools.',
185      );
186    }
187  }
188
189  String _dartSdkVersion;
190
191  String get dartSdkVersion {
192    if (_dartSdkVersion == null) {
193      // Make the version string more customer-friendly.
194      // Changes '2.1.0-dev.8.0.flutter-4312ae32' to '2.1.0 (build 2.1.0-dev.8.0 4312ae32)'
195      final String justVersion = platform.version.split(' ')[0];
196      _dartSdkVersion = justVersion.replaceFirstMapped(RegExp(r'(\d+\.\d+\.\d+)(.+)'), (Match match) {
197        final String noFlutter = match[2].replaceAll('.flutter-', ' ');
198        return '${match[1]} (build ${match[1]}$noFlutter)';
199      });
200    }
201    return _dartSdkVersion;
202  }
203
204  /// The current version of the Flutter engine the flutter tool will download.
205  String get engineRevision {
206    _engineRevision ??= getVersionFor('engine');
207    return _engineRevision;
208  }
209  String _engineRevision;
210
211  static Cache get instance => context.get<Cache>();
212
213  /// Return the top-level directory in the cache; this is `bin/cache`.
214  Directory getRoot() {
215    if (_rootOverride != null)
216      return fs.directory(fs.path.join(_rootOverride.path, 'bin', 'cache'));
217    else
218      return fs.directory(fs.path.join(flutterRoot, 'bin', 'cache'));
219  }
220
221  /// Return a directory in the cache dir. For `pkg`, this will return `bin/cache/pkg`.
222  Directory getCacheDir(String name) {
223    final Directory dir = fs.directory(fs.path.join(getRoot().path, name));
224    if (!dir.existsSync()) {
225      dir.createSync(recursive: true);
226      os.chmod(dir, '755');
227    }
228    return dir;
229  }
230
231  /// Return the top-level directory for artifact downloads.
232  Directory getDownloadDir() => getCacheDir('downloads');
233
234  /// Return the top-level mutable directory in the cache; this is `bin/cache/artifacts`.
235  Directory getCacheArtifacts() => getCacheDir('artifacts');
236
237  /// Get a named directory from with the cache's artifact directory; for example,
238  /// `material_fonts` would return `bin/cache/artifacts/material_fonts`.
239  Directory getArtifactDirectory(String name) {
240    return getCacheArtifacts().childDirectory(name);
241  }
242
243  MapEntry<String, String> get dyLdLibEntry {
244    if (_dyLdLibEntry != null) {
245      return _dyLdLibEntry;
246    }
247    final List<String> paths = <String>[];
248    for (CachedArtifact artifact in _artifacts) {
249      final String currentPath = artifact.dyLdLibPath;
250      if (currentPath.isNotEmpty) {
251        paths.add(currentPath);
252      }
253    }
254    _dyLdLibEntry = MapEntry<String, String>('DYLD_LIBRARY_PATH', paths.join(':'));
255    return _dyLdLibEntry;
256  }
257  MapEntry<String, String> _dyLdLibEntry;
258
259  /// The web sdk has to be co-located with the dart-sdk so that they can share source
260  /// code.
261  Directory getWebSdkDirectory() {
262    return getRoot().childDirectory('flutter_web_sdk');
263  }
264
265  String getVersionFor(String artifactName) {
266    final File versionFile = fs.file(fs.path.join(
267        _rootOverride?.path ?? flutterRoot, 'bin', 'internal',
268        '$artifactName.version'));
269    return versionFile.existsSync() ? versionFile.readAsStringSync().trim() : null;
270  }
271
272  String getStampFor(String artifactName) {
273    final File stampFile = getStampFileFor(artifactName);
274    return stampFile.existsSync() ? stampFile.readAsStringSync().trim() : null;
275  }
276
277  void setStampFor(String artifactName, String version) {
278    getStampFileFor(artifactName).writeAsStringSync(version);
279  }
280
281  File getStampFileFor(String artifactName) {
282    return fs.file(fs.path.join(getRoot().path, '$artifactName.stamp'));
283  }
284
285  /// Returns `true` if either [entity] is older than the tools stamp or if
286  /// [entity] doesn't exist.
287  bool isOlderThanToolsStamp(FileSystemEntity entity) {
288    final File flutterToolsStamp = getStampFileFor('flutter_tools');
289    return isOlderThanReference(entity: entity, referenceFile: flutterToolsStamp);
290  }
291
292  bool isUpToDate() => _artifacts.every((CachedArtifact artifact) => artifact.isUpToDate());
293
294  Future<String> getThirdPartyFile(String urlStr, String serviceName) async {
295    final Uri url = Uri.parse(urlStr);
296    final Directory thirdPartyDir = getArtifactDirectory('third_party');
297
298    final Directory serviceDir = fs.directory(fs.path.join(thirdPartyDir.path, serviceName));
299    if (!serviceDir.existsSync()) {
300      serviceDir.createSync(recursive: true);
301      os.chmod(serviceDir, '755');
302    }
303
304    final File cachedFile = fs.file(fs.path.join(serviceDir.path, url.pathSegments.last));
305    if (!cachedFile.existsSync()) {
306      try {
307        await _downloadFile(url, cachedFile);
308      } catch (e) {
309        throwToolExit('Failed to fetch third-party artifact $url: $e');
310      }
311    }
312
313    return cachedFile.path;
314  }
315
316  /// Update the cache to contain all `requiredArtifacts`.
317  Future<void> updateAll(Set<DevelopmentArtifact> requiredArtifacts) async {
318    if (!_lockEnabled) {
319      return;
320    }
321    try {
322      for (CachedArtifact artifact in _artifacts) {
323        if (!artifact.isUpToDate()) {
324          await artifact.update(requiredArtifacts);
325        }
326      }
327    } on SocketException catch (e) {
328      if (_hostsBlockedInChina.contains(e.address?.host)) {
329        printError(
330          'Failed to retrieve Flutter tool dependencies: ${e.message}.\n'
331          'If you\'re in China, please see this page: '
332          'https://flutter.dev/community/china',
333          emphasis: true,
334        );
335      }
336      rethrow;
337    }
338  }
339
340  Future<bool> areRemoteArtifactsAvailable({
341    String engineVersion,
342    bool includeAllPlatforms = true,
343  }) async {
344    final bool includeAllPlatformsState = cache.includeAllPlatforms;
345    bool allAvailible = true;
346    cache.includeAllPlatforms = includeAllPlatforms;
347    for (CachedArtifact cachedArtifact in _artifacts) {
348      if (cachedArtifact is EngineCachedArtifact) {
349        allAvailible &= await cachedArtifact.checkForArtifacts(engineVersion);
350      }
351    }
352    cache.includeAllPlatforms = includeAllPlatformsState;
353    return allAvailible;
354  }
355}
356
357/// An artifact managed by the cache.
358abstract class CachedArtifact {
359  CachedArtifact(this.name, this.cache, this.developmentArtifacts);
360
361  final String name;
362  final Cache cache;
363
364  // The name of the stamp file. Defaults to the same as the
365  // artifact name.
366  String get stampName => name;
367
368  /// Returns a string to be set as environment DYLD_LIBARY_PATH variable
369  String get dyLdLibPath => '';
370
371  /// All development artifacts this cache provides.
372  final Set<DevelopmentArtifact> developmentArtifacts;
373
374  Directory get location => cache.getArtifactDirectory(name);
375  String get version => cache.getVersionFor(name);
376
377  /// Keep track of the files we've downloaded for this execution so we
378  /// can delete them after completion. We don't delete them right after
379  /// extraction in case [update] is interrupted, so we can restart without
380  /// starting from scratch.
381  final List<File> _downloadedFiles = <File>[];
382
383  bool isUpToDate() {
384    if (!location.existsSync()) {
385      return false;
386    }
387    if (version != cache.getStampFor(stampName)) {
388      return false;
389    }
390    return isUpToDateInner();
391  }
392
393  Future<void> update(Set<DevelopmentArtifact> requiredArtifacts) async {
394    // If the set of required artifacts does not include any from this cache,
395    // then we can claim we are up to date to skip downloading.
396    if (!requiredArtifacts.any(developmentArtifacts.contains)) {
397      printTrace('Artifact $this is not required, skipping update.');
398      return;
399    }
400    if (!location.existsSync()) {
401      try {
402        location.createSync(recursive: true);
403      } on FileSystemException catch (err) {
404        printError(err.toString());
405        throwToolExit(
406          'Failed to create directory for flutter cache at ${location.path}. '
407          'Flutter may be missing permissions in its cache directory.'
408        );
409      }
410    }
411    await updateInner();
412    cache.setStampFor(stampName, version);
413    _removeDownloadedFiles();
414  }
415
416  /// Clear any zip/gzip files downloaded.
417  void _removeDownloadedFiles() {
418    for (File f in _downloadedFiles) {
419      f.deleteSync();
420      for (Directory d = f.parent; d.absolute.path != cache.getDownloadDir().absolute.path; d = d.parent) {
421        if (d.listSync().isEmpty) {
422          d.deleteSync();
423        } else {
424          break;
425        }
426      }
427    }
428  }
429
430  /// Hook method for extra checks for being up-to-date.
431  bool isUpToDateInner() => true;
432
433  /// Template method to perform artifact update.
434  Future<void> updateInner();
435
436  String get _storageBaseUrl {
437    final String overrideUrl = platform.environment['FLUTTER_STORAGE_BASE_URL'];
438    if (overrideUrl == null)
439      return 'https://storage.googleapis.com';
440    _maybeWarnAboutStorageOverride(overrideUrl);
441    return overrideUrl;
442  }
443
444  Uri _toStorageUri(String path) => Uri.parse('$_storageBaseUrl/$path');
445
446  /// Download an archive from the given [url] and unzip it to [location].
447  Future<void> _downloadArchive(String message, Uri url, Directory location, bool verifier(File f), void extractor(File f, Directory d)) {
448    return _withDownloadFile('${flattenNameSubdirs(url)}', (File tempFile) async {
449      if (!verifier(tempFile)) {
450        final Status status = logger.startProgress(message, timeout: timeoutConfiguration.slowOperation);
451        try {
452          await _downloadFile(url, tempFile);
453          status.stop();
454        } catch (exception) {
455          status.cancel();
456          rethrow;
457        }
458      } else {
459        logger.printTrace('$message (cached)');
460      }
461      _ensureExists(location);
462      extractor(tempFile, location);
463    });
464  }
465
466  /// Download a zip archive from the given [url] and unzip it to [location].
467  Future<void> _downloadZipArchive(String message, Uri url, Directory location) {
468    return _downloadArchive(message, url, location, os.verifyZip, os.unzip);
469  }
470
471  /// Download a gzipped tarball from the given [url] and unpack it to [location].
472  Future<void> _downloadZippedTarball(String message, Uri url, Directory location) {
473    return _downloadArchive(message, url, location, os.verifyGzip, os.unpack);
474  }
475
476  /// Create a temporary file and invoke [onTemporaryFile] with the file as
477  /// argument, then add the temporary file to the [_downloadedFiles].
478  Future<void> _withDownloadFile(String name, Future<void> onTemporaryFile(File file)) async {
479    final File tempFile = fs.file(fs.path.join(cache.getDownloadDir().path, name));
480    _downloadedFiles.add(tempFile);
481    await onTemporaryFile(tempFile);
482  }
483}
484
485bool _hasWarnedAboutStorageOverride = false;
486
487void _maybeWarnAboutStorageOverride(String overrideUrl) {
488  if (_hasWarnedAboutStorageOverride)
489    return;
490  logger.printStatus(
491    'Flutter assets will be downloaded from $overrideUrl. Make sure you trust this source!',
492    emphasis: true,
493  );
494  _hasWarnedAboutStorageOverride = true;
495}
496
497/// A cached artifact containing fonts used for Material Design.
498class MaterialFonts extends CachedArtifact {
499  MaterialFonts(Cache cache) : super(
500    'material_fonts',
501    cache,
502    const <DevelopmentArtifact>{ DevelopmentArtifact.universal },
503  );
504
505  @override
506  Future<void> updateInner() {
507    final Uri archiveUri = _toStorageUri(version);
508    return _downloadZipArchive('Downloading Material fonts...', archiveUri, location);
509  }
510}
511
512/// A cached artifact containing the web dart:ui sources, platform dill files,
513/// and libraries.json.
514///
515/// This SDK references code within the regular Dart sdk to reduce download size.
516class FlutterWebSdk extends CachedArtifact {
517  FlutterWebSdk(Cache cache) : super(
518    'flutter_web_sdk',
519    cache,
520    const <DevelopmentArtifact>{ DevelopmentArtifact.web },
521  );
522
523  @override
524  Directory get location => cache.getWebSdkDirectory();
525
526  @override
527  String get version => cache.getVersionFor('engine');
528
529  @override
530  Future<void> updateInner() async {
531    String platformName = 'flutter-web-sdk-';
532    if (platform.isMacOS) {
533      platformName += 'darwin-x64';
534    } else if (platform.isLinux) {
535      platformName += 'linux-x64';
536    } else if (platform.isWindows) {
537      platformName += 'windows-x64';
538    }
539    final Uri url = Uri.parse('$_storageBaseUrl/flutter_infra/flutter/$version/$platformName.zip');
540    await _downloadZipArchive('Downloading Web SDK...', url, location);
541    // This is a temporary work-around for not being able to safely download into a shared directory.
542    for (FileSystemEntity entity in location.listSync(recursive: true)) {
543      if (entity is File) {
544        final List<String> segments = fs.path.split(entity.path);
545        segments.remove('flutter_web_sdk');
546        final String newPath = fs.path.joinAll(segments);
547        final File newFile = fs.file(newPath);
548        if (!newFile.existsSync()) {
549          newFile.createSync(recursive: true);
550        }
551        entity.copySync(newPath);
552      }
553    }
554  }
555}
556
557abstract class EngineCachedArtifact extends CachedArtifact {
558  EngineCachedArtifact(
559    this.stampName,
560    Cache cache,
561    Set<DevelopmentArtifact> requiredArtifacts,
562  ) : super('engine', cache, requiredArtifacts);
563
564  @override
565  final String stampName;
566
567  /// Return a list of (directory path, download URL path) tuples.
568  List<List<String>> getBinaryDirs();
569
570  /// A list of cache directory paths to which the LICENSE file should be copied.
571  List<String> getLicenseDirs();
572
573  /// A list of the dart package directories to download.
574  List<String> getPackageDirs();
575
576  @override
577  bool isUpToDateInner() {
578    final Directory pkgDir = cache.getCacheDir('pkg');
579    for (String pkgName in getPackageDirs()) {
580      final String pkgPath = fs.path.join(pkgDir.path, pkgName);
581      if (!fs.directory(pkgPath).existsSync()) {
582        return false;
583      }
584    }
585
586    for (List<String> toolsDir in getBinaryDirs()) {
587      final Directory dir = fs.directory(fs.path.join(location.path, toolsDir[0]));
588      if (!dir.existsSync()) {
589        return false;
590      }
591    }
592
593    for (String licenseDir in getLicenseDirs()) {
594      final File file = fs.file(fs.path.join(location.path, licenseDir, 'LICENSE'));
595      if (!file.existsSync()) {
596        return false;
597      }
598    }
599    return true;
600  }
601
602  @override
603  Future<void> updateInner() async {
604    final String url = '$_storageBaseUrl/flutter_infra/flutter/$version/';
605
606    final Directory pkgDir = cache.getCacheDir('pkg');
607    for (String pkgName in getPackageDirs()) {
608      await _downloadZipArchive('Downloading package $pkgName...', Uri.parse(url + pkgName + '.zip'), pkgDir);
609    }
610
611    for (List<String> toolsDir in getBinaryDirs()) {
612      final String cacheDir = toolsDir[0];
613      final String urlPath = toolsDir[1];
614      final Directory dir = fs.directory(fs.path.join(location.path, cacheDir));
615      await _downloadZipArchive('Downloading $cacheDir tools...', Uri.parse(url + urlPath), dir);
616
617      _makeFilesExecutable(dir);
618
619      const List<String> frameworkNames = <String>['Flutter', 'FlutterMacOS'];
620      for (String frameworkName in frameworkNames) {
621        final File frameworkZip = fs.file(fs.path.join(dir.path, '$frameworkName.framework.zip'));
622        if (frameworkZip.existsSync()) {
623          final Directory framework = fs.directory(fs.path.join(dir.path, '$frameworkName.framework'));
624          framework.createSync();
625          os.unzip(frameworkZip, framework);
626        }
627      }
628    }
629
630    final File licenseSource = fs.file(fs.path.join(Cache.flutterRoot, 'LICENSE'));
631    for (String licenseDir in getLicenseDirs()) {
632      final String licenseDestinationPath = fs.path.join(location.path, licenseDir, 'LICENSE');
633      await licenseSource.copy(licenseDestinationPath);
634    }
635  }
636
637  Future<bool> checkForArtifacts(String engineVersion) async {
638    engineVersion ??= version;
639    final String url = '$_storageBaseUrl/flutter_infra/flutter/$engineVersion/';
640
641    bool exists = false;
642    for (String pkgName in getPackageDirs()) {
643      exists = await _doesRemoteExist('Checking package $pkgName is available...',
644          Uri.parse(url + pkgName + '.zip'));
645      if (!exists) {
646        return false;
647      }
648    }
649
650    for (List<String> toolsDir in getBinaryDirs()) {
651      final String cacheDir = toolsDir[0];
652      final String urlPath = toolsDir[1];
653      exists = await _doesRemoteExist('Checking $cacheDir tools are available...',
654          Uri.parse(url + urlPath));
655      if (!exists) {
656        return false;
657      }
658    }
659    return true;
660  }
661
662  void _makeFilesExecutable(Directory dir) {
663    os.chmod(dir, 'a+r,a+x');
664    for (FileSystemEntity entity in dir.listSync(recursive: true)) {
665      if (entity is File) {
666        final FileStat stat = entity.statSync();
667        final bool isUserExecutable = ((stat.mode >> 6) & 0x1) == 1;
668        if (entity.basename == 'flutter_tester' || isUserExecutable) {
669          // Make the file readable and executable by all users.
670          os.chmod(entity, 'a+r,a+x');
671        }
672      }
673    }
674  }
675}
676
677
678/// A cached artifact containing the dart:ui source code.
679class FlutterSdk extends EngineCachedArtifact {
680  FlutterSdk(Cache cache) : super(
681    'flutter_sdk',
682    cache,
683    const <DevelopmentArtifact>{ DevelopmentArtifact.universal },
684  );
685
686  @override
687  List<String> getPackageDirs() => const <String>['sky_engine'];
688
689  @override
690  List<List<String>> getBinaryDirs() {
691    return <List<String>>[
692      <String>['common', 'flutter_patched_sdk.zip'],
693      <String>['common', 'flutter_patched_sdk_product.zip'],
694      if (cache.includeAllPlatforms)
695        ...<List<String>>[
696          <String>['windows-x64', 'windows-x64/artifacts.zip'],
697          <String>['linux-x64', 'linux-x64/artifacts.zip'],
698          <String>['darwin-x64', 'darwin-x64/artifacts.zip'],
699        ]
700      else if (platform.isWindows)
701        <String>['windows-x64', 'windows-x64/artifacts.zip']
702      else if (platform.isMacOS)
703        <String>['darwin-x64', 'darwin-x64/artifacts.zip']
704      else if (platform.isLinux)
705        <String>['linux-x64', 'linux-x64/artifacts.zip'],
706    ];
707  }
708
709  @override
710  List<String> getLicenseDirs() => const <String>[];
711}
712
713class MacOSEngineArtifacts extends EngineCachedArtifact {
714  MacOSEngineArtifacts(Cache cache) : super(
715    'macos-sdk',
716    cache,
717    const <DevelopmentArtifact> { DevelopmentArtifact.macOS },
718  );
719
720  @override
721  List<String> getPackageDirs() => const <String>[];
722
723  @override
724  List<List<String>> getBinaryDirs() {
725    if (platform.isMacOS) {
726      return _macOSDesktopBinaryDirs;
727    }
728    return const <List<String>>[];
729  }
730
731  @override
732  List<String> getLicenseDirs() => const <String>[];
733}
734
735class WindowsEngineArtifacts extends EngineCachedArtifact {
736  WindowsEngineArtifacts(Cache cache) : super(
737    'windows-sdk',
738    cache,
739    const <DevelopmentArtifact> { DevelopmentArtifact.windows },
740  );
741
742  @override
743  List<String> getPackageDirs() => const <String>[];
744
745  @override
746  List<List<String>> getBinaryDirs() {
747    if (platform.isWindows) {
748      return _windowsDesktopBinaryDirs;
749    }
750    return const <List<String>>[];
751  }
752
753  @override
754  List<String> getLicenseDirs() => const <String>[];
755}
756
757class LinuxEngineArtifacts extends EngineCachedArtifact {
758  LinuxEngineArtifacts(Cache cache) : super(
759    'linux-sdk',
760    cache,
761    const <DevelopmentArtifact> { DevelopmentArtifact.linux },
762  );
763
764  @override
765  List<String> getPackageDirs() => const <String>[];
766
767  @override
768  List<List<String>> getBinaryDirs() {
769    if (platform.isLinux) {
770      return _linuxDesktopBinaryDirs;
771    }
772    return const <List<String>>[];
773  }
774
775  @override
776  List<String> getLicenseDirs() => const <String>[];
777}
778
779class AndroidEngineArtifacts extends EngineCachedArtifact {
780  AndroidEngineArtifacts(Cache cache) : super(
781    'android-sdk',
782    cache,
783    const <DevelopmentArtifact>{ DevelopmentArtifact.android },
784  );
785
786  @override
787  List<String> getPackageDirs() => const <String>[];
788
789  @override
790  List<List<String>> getBinaryDirs() {
791    return <List<String>>[
792      if (cache.includeAllPlatforms)
793        ...<List<String>>[
794          ..._osxBinaryDirs,
795          ..._linuxBinaryDirs,
796          ..._windowsBinaryDirs,
797          ..._androidBinaryDirs,
798          ..._dartSdks,
799        ]
800      else if (platform.isWindows)
801        ...<List<String>>[
802          ..._windowsBinaryDirs,
803          ..._androidBinaryDirs,
804        ]
805      else if (platform.isMacOS)
806        ...<List<String>>[
807          ..._osxBinaryDirs,
808          ..._androidBinaryDirs,
809        ]
810      else if (platform.isLinux)
811        ...<List<String>>[
812          ..._linuxBinaryDirs,
813          ..._androidBinaryDirs,
814        ]
815    ];
816  }
817
818  @override
819  List<String> getLicenseDirs() { return <String>[]; }
820}
821
822class IOSEngineArtifacts extends EngineCachedArtifact {
823  IOSEngineArtifacts(Cache cache) : super(
824    'ios-sdk',
825    cache,
826    <DevelopmentArtifact>{ DevelopmentArtifact.iOS },
827  );
828
829  @override
830  List<List<String>> getBinaryDirs() {
831    return <List<String>>[
832      if (platform.isMacOS || cache.includeAllPlatforms)
833        ..._iosBinaryDirs,
834    ];
835  }
836
837  @override
838  List<String> getLicenseDirs() {
839    if (cache.includeAllPlatforms || platform.isMacOS) {
840      return const <String>['ios', 'ios-profile', 'ios-release'];
841    }
842    return const <String>[];
843  }
844
845  @override
846  List<String> getPackageDirs() {
847    return <String>[];
848  }
849}
850
851/// A cached artifact containing Gradle Wrapper scripts and binaries.
852///
853/// While this is only required for Android, we need to always download it due
854/// the ensurePlatformSpecificTooling logic.
855class GradleWrapper extends CachedArtifact {
856  GradleWrapper(Cache cache) : super(
857    'gradle_wrapper',
858    cache,
859    const <DevelopmentArtifact>{ DevelopmentArtifact.universal },
860  );
861
862  List<String> get _gradleScripts => <String>['gradlew', 'gradlew.bat'];
863
864  String get _gradleWrapper => fs.path.join('gradle', 'wrapper', 'gradle-wrapper.jar');
865
866  @override
867  Future<void> updateInner() {
868    final Uri archiveUri = _toStorageUri(version);
869    return _downloadZippedTarball('Downloading Gradle Wrapper...', archiveUri, location).then<void>((_) {
870      // Delete property file, allowing templates to provide it.
871      fs.file(fs.path.join(location.path, 'gradle', 'wrapper', 'gradle-wrapper.properties')).deleteSync();
872      // Remove NOTICE file. Should not be part of the template.
873      fs.file(fs.path.join(location.path, 'NOTICE')).deleteSync();
874    });
875  }
876
877  @override
878  bool isUpToDateInner() {
879    final Directory wrapperDir = cache.getCacheDir(fs.path.join('artifacts', 'gradle_wrapper'));
880    if (!fs.directory(wrapperDir).existsSync()) {
881      return false;
882    }
883    for (String scriptName in _gradleScripts) {
884      final File scriptFile = fs.file(fs.path.join(wrapperDir.path, scriptName));
885      if (!scriptFile.existsSync())
886        return false;
887    }
888    final File gradleWrapperJar = fs.file(fs.path.join(wrapperDir.path, _gradleWrapper));
889    if (!gradleWrapperJar.existsSync())
890      return false;
891    return true;
892  }
893}
894
895 const String _cipdBaseUrl =
896    'https://chrome-infra-packages.appspot.com/dl';
897
898/// Common functionality for pulling Fuchsia SDKs.
899abstract class _FuchsiaSDKArtifacts extends CachedArtifact {
900  _FuchsiaSDKArtifacts(Cache cache, String platform)
901      :_path = 'fuchsia/sdk/core/$platform-amd64',
902       super('fuchsia-$platform', cache, const <DevelopmentArtifact> {
903    DevelopmentArtifact.fuchsia,
904  });
905
906  final String _path;
907
908  @override
909  Directory get location => cache.getArtifactDirectory('fuchsia');
910
911  Future<void> _doUpdate() {
912    final String url = '$_cipdBaseUrl/$_path/+/$version';
913    return _downloadZipArchive('Downloading package fuchsia SDK...',
914                               Uri.parse(url), location);
915  }
916}
917
918/// The pre-built flutter runner for Fuchsia development.
919class FlutterRunnerSDKArtifacts extends CachedArtifact {
920  FlutterRunnerSDKArtifacts(Cache cache)
921      : super('flutter_runner', cache, const <DevelopmentArtifact>{
922    DevelopmentArtifact.flutterRunner,
923  });
924
925  @override
926  Directory get location => cache.getArtifactDirectory('flutter_runner');
927
928  @override
929  String get version => cache.getVersionFor('engine');
930
931  @override
932  Future<void> updateInner() async {
933    if (!platform.isLinux && !platform.isMacOS) {
934      return Future<void>.value();
935    }
936    final String url = '$_cipdBaseUrl/flutter/fuchsia/+/git_revision:$version';
937    await _downloadZipArchive('Downloading package flutter runner...',
938        Uri.parse(url), location);
939  }
940}
941
942/// The Fuchsia core SDK for Linux.
943class LinuxFuchsiaSDKArtifacts extends _FuchsiaSDKArtifacts {
944  LinuxFuchsiaSDKArtifacts(Cache cache) : super(cache, 'linux');
945
946  @override
947  Future<void> updateInner() {
948    if (!platform.isLinux) {
949      return Future<void>.value();
950    }
951    return _doUpdate();
952  }
953}
954
955/// The Fuchsia core SDK for MacOS.
956class MacOSFuchsiaSDKArtifacts extends _FuchsiaSDKArtifacts {
957  MacOSFuchsiaSDKArtifacts(Cache cache) : super(cache, 'mac');
958
959  @override
960  Future<void> updateInner() async {
961    if (!platform.isMacOS) {
962      return Future<void>.value();
963    }
964    return _doUpdate();
965  }
966}
967
968/// Cached iOS/USB binary artifacts.
969class IosUsbArtifacts extends CachedArtifact {
970  IosUsbArtifacts(String name, Cache cache) : super(
971    name,
972    cache,
973    // This is universal to ensure every command checks for them first
974    const <DevelopmentArtifact>{ DevelopmentArtifact.universal },
975  );
976
977  static const List<String> artifactNames = <String>[
978    'libimobiledevice',
979    'usbmuxd',
980    'libplist',
981    'openssl',
982    'ideviceinstaller',
983    'ios-deploy',
984  ];
985
986  @override
987  String get dyLdLibPath {
988    return cache.getArtifactDirectory(name).path;
989  }
990
991  @override
992  Future<void> updateInner() {
993    if (!platform.isMacOS) {
994      return Future<void>.value();
995    }
996    final Uri archiveUri = Uri.parse('$_storageBaseUrl/flutter_infra/ios-usb-dependencies/$name/$version/$name.zip');
997    return _downloadZipArchive('Downloading $name...', archiveUri, location);
998  }
999}
1000
1001// Many characters are problematic in filenames, especially on Windows.
1002final Map<int, List<int>> _flattenNameSubstitutions = <int, List<int>>{
1003  r'@'.codeUnitAt(0): '@@'.codeUnits,
1004  r'/'.codeUnitAt(0): '@s@'.codeUnits,
1005  r'\'.codeUnitAt(0): '@bs@'.codeUnits,
1006  r':'.codeUnitAt(0): '@c@'.codeUnits,
1007  r'%'.codeUnitAt(0): '@per@'.codeUnits,
1008  r'*'.codeUnitAt(0): '@ast@'.codeUnits,
1009  r'<'.codeUnitAt(0): '@lt@'.codeUnits,
1010  r'>'.codeUnitAt(0): '@gt@'.codeUnits,
1011  r'"'.codeUnitAt(0): '@q@'.codeUnits,
1012  r'|'.codeUnitAt(0): '@pip@'.codeUnits,
1013  r'?'.codeUnitAt(0): '@ques@'.codeUnits,
1014};
1015
1016/// Given a name containing slashes, colons, and backslashes, expand it into
1017/// something that doesn't.
1018String _flattenNameNoSubdirs(String fileName) {
1019  final List<int> replacedCodeUnits = <int>[
1020    for (int codeUnit in fileName.codeUnits)
1021      ..._flattenNameSubstitutions[codeUnit] ?? <int>[codeUnit],
1022  ];
1023  return String.fromCharCodes(replacedCodeUnits);
1024}
1025
1026@visibleForTesting
1027String flattenNameSubdirs(Uri url) {
1028  final List<String> pieces = <String>[url.host, ...url.pathSegments];
1029  final Iterable<String> convertedPieces = pieces.map<String>(_flattenNameNoSubdirs);
1030  return fs.path.joinAll(convertedPieces);
1031}
1032
1033/// Download a file from the given [url] and write it to [location].
1034Future<void> _downloadFile(Uri url, File location) async {
1035  _ensureExists(location.parent);
1036  final List<int> fileBytes = await fetchUrl(url);
1037  location.writeAsBytesSync(fileBytes, flush: true);
1038}
1039
1040Future<bool> _doesRemoteExist(String message, Uri url) async {
1041  final Status status = logger.startProgress(message, timeout: timeoutConfiguration.slowOperation);
1042  final bool exists = await doesRemoteFileExist(url);
1043  status.stop();
1044  return exists;
1045}
1046
1047/// Create the given [directory] and parents, as necessary.
1048void _ensureExists(Directory directory) {
1049  if (!directory.existsSync()) {
1050    directory.createSync(recursive: true);
1051  }
1052}
1053
1054const List<List<String>> _windowsDesktopBinaryDirs = <List<String>>[
1055  <String>['windows-x64', 'windows-x64/windows-x64-flutter.zip'],
1056  <String>['windows-x64', 'windows-x64/flutter-cpp-client-wrapper.zip'],
1057];
1058
1059const List<List<String>> _linuxDesktopBinaryDirs = <List<String>>[
1060  <String>['linux-x64', 'linux-x64/linux-x64-flutter.zip'],
1061  <String>['linux-x64', 'linux-x64/flutter-cpp-client-wrapper.zip'],
1062];
1063
1064const List<List<String>> _macOSDesktopBinaryDirs = <List<String>>[
1065  <String>['darwin-x64', 'darwin-x64/FlutterMacOS.framework.zip'],
1066];
1067
1068const List<List<String>> _osxBinaryDirs = <List<String>>[
1069  <String>['android-arm-profile/darwin-x64', 'android-arm-profile/darwin-x64.zip'],
1070  <String>['android-arm-release/darwin-x64', 'android-arm-release/darwin-x64.zip'],
1071  <String>['android-arm64-profile/darwin-x64', 'android-arm64-profile/darwin-x64.zip'],
1072  <String>['android-arm64-release/darwin-x64', 'android-arm64-release/darwin-x64.zip'],
1073];
1074
1075const List<List<String>> _linuxBinaryDirs = <List<String>>[
1076  <String>['android-arm-profile/linux-x64', 'android-arm-profile/linux-x64.zip'],
1077  <String>['android-arm-release/linux-x64', 'android-arm-release/linux-x64.zip'],
1078  <String>['android-arm64-profile/linux-x64', 'android-arm64-profile/linux-x64.zip'],
1079  <String>['android-arm64-release/linux-x64', 'android-arm64-release/linux-x64.zip'],
1080];
1081
1082const List<List<String>> _windowsBinaryDirs = <List<String>>[
1083  <String>['android-arm-profile/windows-x64', 'android-arm-profile/windows-x64.zip'],
1084  <String>['android-arm-release/windows-x64', 'android-arm-release/windows-x64.zip'],
1085  <String>['android-arm64-profile/windows-x64', 'android-arm64-profile/windows-x64.zip'],
1086  <String>['android-arm64-release/windows-x64', 'android-arm64-release/windows-x64.zip'],
1087];
1088
1089const List<List<String>> _androidBinaryDirs = <List<String>>[
1090  <String>['android-x86', 'android-x86/artifacts.zip'],
1091  <String>['android-x64', 'android-x64/artifacts.zip'],
1092  <String>['android-arm', 'android-arm/artifacts.zip'],
1093  <String>['android-arm-profile', 'android-arm-profile/artifacts.zip'],
1094  <String>['android-arm-release', 'android-arm-release/artifacts.zip'],
1095  <String>['android-arm64', 'android-arm64/artifacts.zip'],
1096  <String>['android-arm64-profile', 'android-arm64-profile/artifacts.zip'],
1097  <String>['android-arm64-release', 'android-arm64-release/artifacts.zip'],
1098];
1099
1100const List<List<String>> _iosBinaryDirs = <List<String>>[
1101  <String>['ios', 'ios/artifacts.zip'],
1102  <String>['ios-profile', 'ios-profile/artifacts.zip'],
1103  <String>['ios-release', 'ios-release/artifacts.zip'],
1104];
1105
1106const List<List<String>> _dartSdks = <List<String>> [
1107  <String>['darwin-x64', 'dart-sdk-darwin-x64.zip'],
1108  <String>['linux-x64', 'dart-sdk-linux-x64.zip'],
1109  <String>['windows-x64', 'dart-sdk-windows-x64.zip'],
1110];
1111