| // Copyright (c) 2012, the Dart project authors. Please see the AUTHORS file |
| // for details. 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:io'; |
| |
| import 'package:path/path.dart' as p; |
| import 'package:pub_semver/pub_semver.dart'; |
| |
| import 'authentication/token_store.dart'; |
| import 'exceptions.dart'; |
| import 'io.dart'; |
| import 'io.dart' as io show createTempDir; |
| import 'log.dart' as log; |
| import 'package.dart'; |
| import 'package_name.dart'; |
| import 'pubspec.dart'; |
| import 'source.dart'; |
| import 'source/cached.dart'; |
| import 'source/git.dart'; |
| import 'source/hosted.dart'; |
| import 'source/path.dart'; |
| import 'source/sdk.dart'; |
| import 'source/unknown.dart'; |
| import 'utils.dart'; |
| |
| /// The system-wide cache of downloaded packages. |
| /// |
| /// This cache contains all packages that are downloaded from the internet. |
| /// Packages that are available locally (e.g. path dependencies) don't use this |
| /// cache. |
| class SystemCache { |
| /// The root directory where this package cache is located. |
| String get rootDir => _rootDir ??= defaultDir; |
| String? _rootDir; |
| |
| String rootDirForSource(CachedSource source) => p.join(rootDir, source.name); |
| |
| String get tempDir => p.join(rootDir, '_temp'); |
| |
| static String defaultDir = (() { |
| if (Platform.environment.containsKey('PUB_CACHE')) { |
| return p.absolute(Platform.environment['PUB_CACHE']!); |
| } else if (Platform.isWindows) { |
| // %LOCALAPPDATA% is used as the cache location over %APPDATA%, because |
| // the latter is synchronised between devices when the user roams between |
| // them, whereas the former is not. |
| final localAppData = Platform.environment['LOCALAPPDATA']; |
| if (localAppData == null) { |
| dataError(''' |
| Could not find the pub cache. No `LOCALAPPDATA` environment variable exists. |
| Consider setting the `PUB_CACHE` variable manually. |
| '''); |
| } |
| return p.join(localAppData, 'Pub', 'Cache'); |
| } else { |
| final home = Platform.environment['HOME']; |
| if (home == null) { |
| dataError(''' |
| Could not find the pub cache. No `HOME` environment variable exists. |
| Consider setting the `PUB_CACHE` variable manually. |
| '''); |
| } |
| return p.join(home, '.pub-cache'); |
| } |
| })(); |
| |
| /// The available sources. |
| late final _sources = Map<String, Source>.fromIterable( |
| [hosted, git, path, sdk], |
| key: (source) => (source as Source).name, |
| ); |
| |
| Source sources(String? name) { |
| return name == null |
| ? defaultSource |
| : (_sources[name] ?? UnknownSource(name)); |
| } |
| |
| Source get defaultSource => hosted; |
| |
| /// The built-in Git source. |
| GitSource get git => GitSource.instance; |
| |
| /// The built-in hosted source. |
| HostedSource get hosted => HostedSource.instance; |
| |
| /// The built-in path source bound to this cache. |
| PathSource get path => PathSource.instance; |
| |
| /// The built-in SDK source bound to this cache. |
| SdkSource get sdk => SdkSource.instance; |
| |
| /// The default credential store. |
| /// TODO(sigurdm): this does not really belong in the cache. |
| final TokenStore tokenStore; |
| |
| /// If true, cached sources will attempt to use the cached packages for |
| /// resolution. |
| final bool isOffline; |
| |
| /// Creates a system cache and registers all sources in [sources]. |
| /// |
| /// If [isOffline] is `true`, then the offline hosted source will be used. |
| /// Defaults to `false`. |
| SystemCache({String? rootDir, this.isOffline = false}) |
| : _rootDir = rootDir, |
| tokenStore = TokenStore(dartConfigDir); |
| |
| /// Loads the package identified by [id]. |
| /// |
| /// Throws an [ArgumentError] if [id] has an invalid source. |
| Package load(PackageId id) { |
| return Package.load( |
| getDirectory(id), |
| loadPubspec: Pubspec.loadRootWithSources(sources), |
| expectedName: id.name, |
| ); |
| } |
| |
| /// Create a new temporary directory within the system cache. |
| /// |
| /// The system cache maintains its own temporary directory that it uses to |
| /// stage packages into while downloading. It uses this instead of the OS's |
| /// system temp directory to ensure that it's on the same volume as the pub |
| /// system cache so that it can move the directory from it. |
| String createTempDir() { |
| final temp = ensureDir(tempDir); |
| return io.createTempDir(temp, 'dir'); |
| } |
| |
| /// Deletes the system cache's internal temp directory. |
| void deleteTempDir() { |
| log.fine('Clean up system cache temp directory $tempDir.'); |
| if (dirExists(tempDir)) deleteEntry(tempDir); |
| } |
| |
| /// An in-memory cache of pubspecs described by [describe]. |
| final cachedPubspecs = <PackageId, Pubspec>{}; |
| |
| /// Loads the (possibly remote) pubspec for the package version identified by |
| /// [id]. |
| /// |
| /// This may be called for packages that have not yet been downloaded during |
| /// the version resolution process. Its results are automatically memoized. |
| /// |
| /// Throws a [DataException] if the pubspec's version doesn't match [id]'s |
| /// version. |
| Future<Pubspec> describe(PackageId id) async { |
| final pubspec = cachedPubspecs[id] ??= await id.source.doDescribe(id, this); |
| if (pubspec.version != id.version) { |
| throw PackageNotFoundException( |
| 'the pubspec for $id has version ${pubspec.version}', |
| ); |
| } |
| return pubspec; |
| } |
| |
| /// Get the IDs of all versions that match [ref]. |
| /// |
| /// Note that this does *not* require the packages to be downloaded locally, |
| /// which is the point. This is used during version resolution to determine |
| /// which package versions are available to be downloaded (or already |
| /// downloaded). |
| /// |
| /// By default, this assumes that each description has a single version and |
| /// uses [describe] to get that version. |
| /// |
| /// If [maxAge] is given answers can be taken from cache - up to that age old. |
| /// |
| /// If given, the [allowedRetractedVersion] is the only version which can be |
| /// selected even if it is marked as retracted. Otherwise, all the returned |
| /// IDs correspond to non-retracted versions. |
| Future<List<PackageId>> getVersions( |
| PackageRef ref, { |
| Duration? maxAge, |
| Version? allowedRetractedVersion, |
| }) async { |
| var versions = await ref.source.doGetVersions(ref, maxAge, this); |
| |
| versions = (await Future.wait( |
| versions.map((id) async { |
| final packageStatus = await ref.source.status( |
| id.toRef(), |
| id.version, |
| this, |
| maxAge: maxAge, |
| ); |
| if (!packageStatus.isRetracted || |
| id.version == allowedRetractedVersion) { |
| return id; |
| } |
| return null; |
| }), |
| )) |
| .nonNulls |
| .toList(); |
| |
| return versions; |
| } |
| |
| /// Returns the directory where this package can (or could) be found locally. |
| /// |
| /// If the source is cached, this will be a path in the system cache. |
| /// |
| /// If id is a relative path id, the directory will be relative from |
| /// [relativeFrom]. Returns an absolute path if [relativeFrom] is not passed. |
| String getDirectory(PackageId id, {String? relativeFrom}) { |
| return id.source.doGetDirectory(id, this, relativeFrom: relativeFrom); |
| } |
| |
| /// Downloads a cached package identified by [id] to the cache. |
| /// |
| /// [id] must refer to a cached package. |
| /// |
| /// If [allowOutdatedHashChecks] is `true` we use a cached version listing |
| /// response if present instead of probing the server. Not probing allows for |
| /// `pub get` with a filled cache to be a fast case that doesn't require any |
| /// new version-listings. |
| /// |
| /// Returns [id] with an updated [ResolvedDescription], this can be different |
| /// if the content-hash changed while downloading. |
| Future<DownloadPackageResult> downloadPackage(PackageId id) async { |
| final source = id.source; |
| assert(source is CachedSource); |
| final result = await (source as CachedSource).downloadToSystemCache( |
| id, |
| this, |
| ); |
| |
| // We only update the README.md in the cache when a change to the cache has |
| // happened. This is: |
| // * to avoid failing if used with a read-only cache, and |
| // * because the cost of writing a single file is negligible compared to |
| // downloading a package, but might be significant in the fast-case where |
| // a the cache is already valid. |
| if (result.didUpdate) { |
| maintainCache(); |
| } |
| return result; |
| } |
| |
| /// Get the latest version of [package]. |
| /// |
| /// Will consider _prereleases_ if: |
| /// * [allowPrereleases] is true, or, |
| /// * If [version] is non-null and is a prerelease version and there are no |
| /// later stable version we return a prerelease version if it exists. |
| /// |
| /// Returns `null`, if unable to find the package or if [package] is `null`. |
| Future<PackageId?> getLatest( |
| PackageRef? package, { |
| Version? version, |
| bool allowPrereleases = false, |
| }) async { |
| if (package == null) { |
| return null; |
| } |
| |
| final List<PackageId> available; |
| try { |
| // TODO: Pass some maxAge to getVersions |
| available = await getVersions(package); |
| } on PackageNotFoundException { |
| return null; |
| } |
| if (available.isEmpty) { |
| return null; |
| } |
| |
| available.sort( |
| allowPrereleases |
| ? (x, y) => x.version.compareTo(y.version) |
| : (x, y) => Version.prioritize(x.version, y.version), |
| ); |
| var latest = available.last; |
| |
| if (version != null && version.isPreRelease && version > latest.version) { |
| available.sort((x, y) => x.version.compareTo(y.version)); |
| latest = available.last; |
| } |
| |
| // There should be exactly one entry in [available] matching [latest] |
| assert(available.where((id) => id.version == latest.version).length == 1); |
| |
| return latest; |
| } |
| |
| /// Removes all contents of the system cache. |
| /// |
| /// Rewrites the README.md. |
| void clean() { |
| deleteEntry(rootDir); |
| ensureDir(rootDir); |
| maintainCache(); |
| } |
| |
| /// Tasks that ensures the cache is in a good condition. |
| /// Should be called whenever an operation updates the cache. |
| void maintainCache() { |
| /// We only want to do this once per run. |
| if (_hasMaintainedCache) return; |
| _hasMaintainedCache = true; |
| _ensureReadme(); |
| _checkOldCacheLocation(); |
| } |
| |
| /// Check for the presence of a cache at the legacy location |
| /// `%APPDATA$\Pub\Cache`. |
| /// |
| /// If it is present, give a warning and write a DEPRECATED.md in that cache. |
| /// |
| /// If DEPRECATED.md is less than 7 days old, we don't repeat the warning. |
| void _checkOldCacheLocation() { |
| // Background: |
| // Prior to Dart 2.8 the default location for the PUB_CACHE on Windows was: |
| // %APPDATA%\Pub\Cache |
| // |
| // Start Dart 2.8 pub started migrating the default PUB_CACHE location to: |
| // %LOCALAPPDATA%\Pub\Cache |
| // That is: |
| // * If a pub-cache existed in `%LOCALAPPDATA%\Pub\Cache` then it |
| // would be used. |
| // * If a pub-cache existed in `%APPDATA%\Pub\Cache` then it would be |
| // used, unless a pub-cache in `%LOCALAPPDATA%\Pub\Cache` had been found. |
| // * If no pub-cache was found, a new empty pub-cache was created in |
| // `%LOCALAPPDATA%\Pub\Cache`. |
| // |
| // Starting in Dart 3.0 pub will no-longer look for a pub-cache in |
| // `%APPDATA%\Pub\Cache`. Instead it will always use the new location, |
| // `%LOCALAPPDATA%\Pub\Cache`, as default PUB_CACHE location. |
| // |
| // Using `%APPDATA%` caused the pub-cache to be copied with the user-profile, |
| // when using a networked Windows setup where users can login on multiple |
| // machines. This is undesirable because you are moving a lot of bytes over |
| // the network and onto whatever servers are storing the user profiles. |
| // |
| // Thus, we migrated to storing the pub-cache in `%LOCALAPPDATA%`. |
| // And finished the migration in Dart 3 to keep things simple. |
| if (!Platform.isWindows) return; |
| |
| final appData = Platform.environment['APPDATA']; |
| if (appData == null) return; |
| final legacyCacheLocation = p.join(appData, 'Pub', 'Cache'); |
| final legacyCacheDeprecatedFile = |
| p.join(legacyCacheLocation, 'DEPRECATED.md'); |
| final stat = tryStatFile(legacyCacheDeprecatedFile); |
| if ((stat == null || |
| DateTime.now().difference(stat.changed) > Duration(days: 7)) && |
| dirExists(legacyCacheLocation)) { |
| log.warning(''' |
| Found a legacy Pub cache at $legacyCacheLocation. Pub is using $defaultDir. |
| |
| Consider deleting the legacy cache. |
| |
| See https://dart.dev/resources/dart-3-migration#other-tools-changes for details. |
| '''); |
| try { |
| writeTextFile(legacyCacheDeprecatedFile, ''' |
| As of Dart 3 this pub cache is no longer used by Dart/Flutter. |
| |
| Consider deleting it, if you are not using Dart versions earlier than 2.8.0. |
| |
| See https://dart.dev/resources/dart-3-migration#other-tools-changes for details. |
| '''); |
| } on Exception catch (e) { |
| // Failing to write the DEPRECATED.md file should not disrupt other |
| // operations. |
| log.fine('Failed to write $legacyCacheDeprecatedFile: $e'); |
| } |
| } |
| } |
| |
| /// Write a README.md file in the root of the cache directory to document the |
| /// contents of the folder. |
| /// |
| /// This should only be called when we are doing another operation that is |
| /// modifying the `PUB_CACHE`. This ensures that users won't experience |
| /// permission errors because we writing a `README.md` file, in a flow that |
| /// the user expected wouldn't have issues with a read-only `PUB_CACHE`. |
| void _ensureReadme() { |
| final readmePath = p.join(rootDir, 'README.md'); |
| try { |
| writeTextFile(readmePath, ''' |
| Pub Package Cache |
| ================= |
| |
| This folder is used by Pub to store cached packages used in Dart / Flutter |
| projects. |
| |
| The contents of this folder should only be modified using the `dart pub` and |
| `flutter pub` commands. |
| |
| Modifying this folder manually can lead to inconsistent behavior. |
| |
| For details on how manage the `PUB_CACHE`, see: |
| https://dart.dev/go/pub-cache |
| '''); |
| } on Exception catch (e) { |
| // Failing to write the README.md should not disrupt other operations. |
| log.fine('Failed to write README.md in PUB_CACHE: $e'); |
| } |
| } |
| |
| bool _hasMaintainedCache = false; |
| } |
| |
| typedef SourceRegistry = Source Function(String? name); |