| // Copyright 2014 The Flutter 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'; |
| import 'dart:collection'; |
| |
| import 'package:meta/meta.dart'; |
| |
| import '../base/common.dart'; |
| import '../base/context.dart'; |
| import '../base/file_system.dart'; |
| import '../base/logger.dart'; |
| import '../base/net.dart'; |
| import '../base/task_queue.dart'; |
| import '../cache.dart'; |
| import '../dart/pub.dart'; |
| import '../globals.dart' as globals; |
| import '../project.dart'; |
| import '../runner/flutter_command.dart'; |
| |
| /// Map from package name to package version, used to artificially pin a pub |
| /// package version in cases when upgrading to the latest breaks Flutter. |
| /// |
| /// These version pins must be pins, not ranges! Allowing these to be ranges |
| /// defeats the whole purpose of pinning all our dependencies, which is to |
| /// prevent upstream changes from causing our CI to fail randomly in ways |
| /// unrelated to the commits. It also, more importantly, risks breaking users |
| /// in ways that prevent them from ever upgrading Flutter again! |
| const Map<String, String> kManuallyPinnedDependencies = <String, String>{ |
| // Add pinned packages here. Please leave a comment explaining why. |
| 'flutter_gallery_assets': '1.0.2', // Tests depend on the exact version. |
| 'flutter_template_images': '4.2.0', // Must always exactly match flutter_tools template. |
| 'video_player': '2.2.11', |
| // Could potentially break color scheme tests on upgrade, |
| // so pin and manually update as needed. |
| 'material_color_utilities': '0.2.0', |
| // https://github.com/flutter/flutter/issues/111304 |
| 'url_launcher_android': '6.0.17', |
| // https://github.com/flutter/flutter/issues/115660 |
| 'archive': '3.3.2', |
| // https://github.com/flutter/flutter/issues/116376 |
| 'path_provider_android': '2.0.21', |
| // https://github.com/flutter/flutter/issues/117163 |
| 'intl': '0.17.0', |
| }; |
| |
| class UpdatePackagesCommand extends FlutterCommand { |
| UpdatePackagesCommand() { |
| argParser |
| ..addFlag( |
| 'force-upgrade', |
| help: 'Attempt to update all the dependencies to their latest versions.\n' |
| 'This will actually modify the pubspec.yaml files in your checkout.', |
| negatable: false, |
| ) |
| ..addFlag( |
| 'paths', |
| help: 'Finds paths in the dependency chain leading from package specified ' |
| 'in "--from" to package specified in "--to".', |
| negatable: false, |
| ) |
| ..addOption( |
| 'from', |
| help: 'Used with "--dependency-path". Specifies the package to begin ' |
| 'searching dependency path from.', |
| ) |
| ..addOption( |
| 'to', |
| help: 'Used with "--dependency-path". Specifies the package that the ' |
| 'sought-after dependency path leads to.', |
| ) |
| ..addFlag( |
| 'transitive-closure', |
| help: 'Prints the dependency graph that is the transitive closure of ' |
| 'packages the Flutter SDK depends on.', |
| negatable: false, |
| ) |
| ..addFlag( |
| 'consumer-only', |
| help: 'Only prints the dependency graph that is the transitive closure ' |
| 'that a consumer of the Flutter SDK will observe (when combined ' |
| 'with transitive-closure).', |
| negatable: false, |
| ) |
| ..addFlag( |
| 'verify-only', |
| help: 'Verifies the package checksum without changing or updating deps.', |
| negatable: false, |
| ) |
| ..addFlag( |
| 'offline', |
| help: 'Use cached packages instead of accessing the network.', |
| negatable: false, |
| ) |
| ..addFlag( |
| 'crash', |
| help: 'For Flutter CLI testing only, forces this command to throw an unhandled exception.', |
| negatable: false, |
| ) |
| ..addOption( |
| 'jobs', |
| abbr: 'j', |
| help: 'Causes the "pub get" runs to happen concurrently on this many ' |
| 'CPUs. Defaults to the number of CPUs that this machine has.', |
| ) |
| ..addOption( |
| 'synthetic-package-path', |
| help: 'Write the synthetic monolithic pub package generated to do ' |
| 'version solving to a persistent path. By default, a temporary ' |
| 'directory that is deleted before the command exits. By ' |
| 'providing this path, a Flutter maintainer can inspect further ' |
| 'exactly how version solving was achieved.', |
| ); |
| } |
| |
| @override |
| final String name = 'update-packages'; |
| |
| @override |
| final String description = 'Update the packages inside the Flutter repo. ' |
| 'This is intended for CI and repo maintainers. ' |
| 'Normal Flutter developers should not have to ' |
| 'use this command.'; |
| |
| @override |
| final List<String> aliases = <String>['upgrade-packages']; |
| |
| @override |
| final bool hidden = true; |
| |
| |
| // Lazy-initialize the net utilities with values from the context. |
| late final Net _net = Net( |
| httpClientFactory: context.get<HttpClientFactory>(), |
| logger: globals.logger, |
| platform: globals.platform, |
| ); |
| |
| Future<void> _downloadCoverageData() async { |
| final String urlBase = globals.platform.environment['FLUTTER_STORAGE_BASE_URL'] ?? 'https://storage.googleapis.com'; |
| final Uri coverageUri = Uri.parse('$urlBase/flutter_infra_release/flutter/coverage/lcov.info'); |
| final List<int>? data = await _net.fetchUrl( |
| coverageUri, |
| maxAttempts: 3, |
| ); |
| if (data == null) { |
| throwToolExit('Failed to fetch coverage data from $coverageUri'); |
| } |
| final String coverageDir = globals.fs.path.join( |
| Cache.flutterRoot!, |
| 'packages/flutter/coverage', |
| ); |
| globals.fs.file(globals.fs.path.join(coverageDir, 'lcov.base.info')) |
| ..createSync(recursive: true) |
| ..writeAsBytesSync(data, flush: true); |
| globals.fs.file(globals.fs.path.join(coverageDir, 'lcov.info')) |
| ..createSync(recursive: true) |
| ..writeAsBytesSync(data, flush: true); |
| } |
| |
| late final Directory _syntheticPackageDir = (() { |
| final String? optionPath = stringArg('synthetic-package-path'); |
| if (optionPath == null) { |
| return globals.fs.systemTempDirectory.createTempSync('flutter_update_packages.'); |
| } |
| final Directory syntheticPackageDir = globals.fs.directory(optionPath); |
| if (!syntheticPackageDir.existsSync()) { |
| syntheticPackageDir.createSync(recursive: true); |
| } |
| globals.printStatus( |
| 'The synthetic package with all pub dependencies across the repo will ' |
| 'be written to ${syntheticPackageDir.absolute.path}.', |
| ); |
| return syntheticPackageDir; |
| })(); |
| |
| @override |
| Future<FlutterCommandResult> runCommand() async { |
| final List<Directory> packages = runner!.getRepoPackages(); |
| |
| final bool forceUpgrade = boolArgDeprecated('force-upgrade'); |
| final bool isPrintPaths = boolArgDeprecated('paths'); |
| final bool isPrintTransitiveClosure = boolArgDeprecated('transitive-closure'); |
| final bool isVerifyOnly = boolArgDeprecated('verify-only'); |
| final bool isConsumerOnly = boolArgDeprecated('consumer-only'); |
| final bool offline = boolArgDeprecated('offline'); |
| final bool doUpgrade = forceUpgrade || isPrintPaths || isPrintTransitiveClosure; |
| |
| if (boolArgDeprecated('crash')) { |
| throw StateError('test crash please ignore.'); |
| } |
| |
| if (forceUpgrade && offline) { |
| throwToolExit( |
| '--force-upgrade cannot be used with the --offline flag' |
| ); |
| } |
| |
| // "consumer" packages are those that constitute our public API (e.g. flutter, flutter_test, flutter_driver, flutter_localizations, integration_test). |
| if (isConsumerOnly) { |
| if (!isPrintTransitiveClosure) { |
| throwToolExit( |
| '--consumer-only can only be used with the --transitive-closure flag' |
| ); |
| } |
| // Only retain flutter, flutter_test, flutter_driver, and flutter_localizations. |
| const List<String> consumerPackages = <String>['flutter', 'flutter_test', 'flutter_driver', 'flutter_localizations', 'integration_test']; |
| // ensure we only get flutter/packages |
| packages.retainWhere((Directory directory) { |
| return consumerPackages.any((String package) { |
| return directory.path.endsWith('packages${globals.fs.path.separator}$package'); |
| }); |
| }); |
| } |
| |
| if (isVerifyOnly) { |
| _verifyPubspecs(packages); |
| return FlutterCommandResult.success(); |
| } |
| |
| if (doUpgrade) { |
| // This feature attempts to collect all the packages used across all the |
| // pubspec.yamls in the repo (including via transitive dependencies), and |
| // find the latest version of each that can be used while keeping each |
| // such package fixed at a single version across all the pubspec.yamls. |
| globals.printStatus('Upgrading packages...'); |
| } |
| |
| // First, collect the dependencies: |
| final List<PubspecYaml> pubspecs = <PubspecYaml>[]; |
| final Map<String, PubspecDependency> explicitDependencies = <String, PubspecDependency>{}; |
| final Map<String, PubspecDependency> allDependencies = <String, PubspecDependency>{}; |
| final Set<String> specialDependencies = <String>{}; |
| _collectDependencies( |
| packages: packages, |
| pubspecs: pubspecs, |
| explicitDependencies: explicitDependencies, |
| allDependencies: allDependencies, |
| specialDependencies: specialDependencies, |
| doUpgrade: doUpgrade, |
| ); |
| |
| // Now that we have all the dependencies we care about, we are going to |
| // create a fake package and then run either "pub upgrade", if requested, |
| // followed by "pub get" on it. If upgrading, the pub tool will attempt to |
| // bring these dependencies up to the most recent possible versions while |
| // honoring all their constraints. If not upgrading the pub tool will only |
| // attempt to download any necessary package versions to the pub cache to |
| // warm the cache. |
| final PubDependencyTree tree = PubDependencyTree(); // object to collect results |
| await _generateFakePackage( |
| tempDir: _syntheticPackageDir, |
| dependencies: doUpgrade ? explicitDependencies.values : allDependencies.values, |
| pubspecs: pubspecs, |
| tree: tree, |
| doUpgrade: doUpgrade, |
| ); |
| |
| // Only delete the synthetic package if it was done in a temp directory |
| if (stringArg('synthetic-package-path') == null) { |
| _syntheticPackageDir.deleteSync(recursive: true); |
| } |
| |
| if (doUpgrade) { |
| final bool done = _upgradePubspecs( |
| tree: tree, |
| pubspecs: pubspecs, |
| explicitDependencies: explicitDependencies, |
| specialDependencies: specialDependencies, |
| ); |
| |
| if (done) { |
| // Complete early if we were just printing data. |
| return FlutterCommandResult.success(); |
| } |
| } |
| |
| await _runPubGetOnPackages(packages); |
| |
| return FlutterCommandResult.success(); |
| } |
| |
| void _verifyPubspecs(List<Directory> packages) { |
| bool needsUpdate = false; |
| globals.printStatus('Verifying pubspecs...'); |
| for (final Directory directory in packages) { |
| final PubspecYaml pubspec = PubspecYaml(directory); |
| globals.printTrace('Reading pubspec.yaml from ${directory.path}'); |
| if (pubspec.checksum.value == null) { |
| // If the checksum is invalid or missing, we can just ask them run to run |
| // upgrade again to compute it. |
| globals.printWarning( |
| 'Warning: pubspec in ${directory.path} has out of date dependencies. ' |
| 'Please run "flutter update-packages --force-upgrade" to update them correctly.' |
| ); |
| needsUpdate = true; |
| } |
| // all dependencies in the pubspec sorted lexically. |
| final Map<String, String> checksumDependencies = <String, String>{}; |
| for (final PubspecLine data in pubspec.inputData) { |
| if (data is PubspecDependency && data.kind == DependencyKind.normal) { |
| checksumDependencies[data.name] = data.version; |
| } |
| } |
| final String checksum = _computeChecksum(checksumDependencies.keys, (String name) => checksumDependencies[name]!); |
| if (checksum != pubspec.checksum.value) { |
| // If the checksum doesn't match, they may have added or removed some dependencies. |
| // we need to run update-packages to recapture the transitive deps. |
| globals.printWarning( |
| 'Warning: pubspec in ${directory.path} has updated or new dependencies. ' |
| 'Please run "flutter update-packages --force-upgrade" to update them correctly ' |
| '(checksum ${pubspec.checksum.value} != $checksum).' |
| ); |
| needsUpdate = true; |
| } else { |
| // everything is correct in the pubspec. |
| globals.printTrace('pubspec in ${directory.path} is up to date!'); |
| } |
| } |
| if (needsUpdate) { |
| throwToolExit( |
| 'Warning: one or more pubspecs have invalid dependencies. ' |
| 'Please run "flutter update-packages --force-upgrade" to update them correctly.', |
| exitCode: 1, |
| ); |
| } |
| globals.printStatus('All pubspecs were up to date.'); |
| } |
| |
| void _collectDependencies({ |
| required List<Directory> packages, |
| required List<PubspecYaml> pubspecs, |
| required Set<String> specialDependencies, |
| required Map<String, PubspecDependency> explicitDependencies, |
| required Map<String, PubspecDependency> allDependencies, |
| required bool doUpgrade, |
| }) { |
| // Visit all the directories with pubspec.yamls we care about. |
| for (final Directory directory in packages) { |
| if (doUpgrade) { |
| globals.printTrace('Reading pubspec.yaml from: ${directory.path}'); |
| } |
| final PubspecYaml pubspec = PubspecYaml(directory); // this parses the pubspec.yaml |
| pubspecs.add(pubspec); // remember it for later |
| for (final PubspecDependency dependency in pubspec.allDependencies) { |
| if (allDependencies.containsKey(dependency.name)) { |
| // If we've seen the dependency before, make sure that we are |
| // importing it the same way. There's several ways to import a |
| // dependency. Hosted (from pub via version number), by path (e.g. |
| // pointing at the version of a package we get from the Dart SDK |
| // that we download with Flutter), by SDK (e.g. the "flutter" |
| // package is explicitly from "sdk: flutter"). |
| // |
| // This makes sure that we don't import a package in two different |
| // ways, e.g. by saying "sdk: flutter" in one pubspec.yaml and |
| // saying "path: ../../..." in another. |
| final PubspecDependency previous = allDependencies[dependency.name]!; |
| if (dependency.kind != previous.kind || dependency._lockTarget != previous._lockTarget) { |
| throwToolExit( |
| 'Inconsistent requirements around ${dependency.name}; ' |
| 'saw ${dependency.kind} (${dependency._lockTarget}) in "${dependency.sourcePath}" ' |
| 'and ${previous.kind} (${previous._lockTarget}) in "${previous.sourcePath}".' |
| ); |
| } |
| if (dependency.version != previous.version) { |
| globals.printError( |
| 'Requiring multiple versions: multiple versions required by ${dependency.name}; ' |
| 'saw ${dependency.version} in "${dependency.sourcePath}" ' |
| 'and ${previous.version} in "${previous.sourcePath}".' |
| ); |
| } |
| } |
| allDependencies[dependency.name] = dependency; |
| } |
| for (final PubspecDependency dependency in pubspec.allExplicitDependencies) { |
| if (explicitDependencies.containsKey(dependency.name)) { |
| // If we've seen the dependency before, make sure that we are |
| // importing it the same way. There's several ways to import a |
| // dependency. Hosted (from pub via version number), by path (e.g. |
| // pointing at the version of a package we get from the Dart SDK |
| // that we download with Flutter), by SDK (e.g. the "flutter" |
| // package is explicitly from "sdk: flutter"). |
| // |
| // This makes sure that we don't import a package in two different |
| // ways, e.g. by saying "sdk: flutter" in one pubspec.yaml and |
| // saying "path: ../../..." in another. |
| final PubspecDependency previous = explicitDependencies[dependency.name]!; |
| if (dependency.kind != previous.kind || dependency._lockTarget != previous._lockTarget) { |
| throwToolExit( |
| 'Inconsistent requirements around ${dependency.name}; ' |
| 'saw ${dependency.kind} (${dependency._lockTarget}) in "${dependency.sourcePath}" ' |
| 'and ${previous.kind} (${previous._lockTarget}) in "${previous.sourcePath}".' |
| ); |
| } |
| } |
| // Remember this dependency by name so we can look it up again. |
| explicitDependencies[dependency.name] = dependency; |
| // Normal dependencies are those we get from pub. The others we |
| // already implicitly pin since we pull down one version of the |
| // Flutter and Dart SDKs, so we track which those are here so that we |
| // can omit them from our list of pinned dependencies later. |
| if (dependency.kind != DependencyKind.normal) { |
| specialDependencies.add(dependency.name); |
| } |
| } |
| } |
| } |
| |
| Future<void> _generateFakePackage({ |
| required Directory tempDir, |
| required Iterable<PubspecDependency> dependencies, |
| required List<PubspecYaml> pubspecs, |
| required PubDependencyTree tree, |
| required bool doUpgrade, |
| }) async { |
| Directory? temporaryFlutterSdk; |
| final Directory syntheticPackageDir = tempDir.childDirectory('synthetic_package'); |
| final File fakePackage = _pubspecFor(syntheticPackageDir); |
| fakePackage.createSync(recursive: true); |
| fakePackage.writeAsStringSync( |
| generateFakePubspec( |
| dependencies, |
| doUpgrade: doUpgrade, |
| ), |
| ); |
| // Create a synthetic flutter SDK so that transitive flutter SDK |
| // constraints are not affected by this upgrade. |
| if (doUpgrade) { |
| temporaryFlutterSdk = createTemporaryFlutterSdk( |
| globals.logger, |
| globals.fs, |
| globals.fs.directory(Cache.flutterRoot), |
| pubspecs, |
| tempDir, |
| ); |
| } |
| |
| // Next we run "pub get" on it in order to force the download of any |
| // needed packages to the pub cache, upgrading if requested. |
| await pub.get( |
| context: PubContext.updatePackages, |
| project: FlutterProject.fromDirectory(syntheticPackageDir), |
| upgrade: doUpgrade, |
| offline: boolArgDeprecated('offline'), |
| flutterRootOverride: temporaryFlutterSdk?.path, |
| printProgress: false, |
| ); |
| |
| if (doUpgrade) { |
| // If upgrading, we run "pub deps --style=compact" on the result. We |
| // pipe all the output to tree.fill(), which parses it so that it can |
| // create a graph of all the dependencies so that we can figure out the |
| // transitive dependencies later. It also remembers which version was |
| // selected for each package. |
| await pub.batch( |
| <String>['deps', '--style=compact'], |
| context: PubContext.updatePackages, |
| directory: syntheticPackageDir.path, |
| filter: tree.fill, |
| ); |
| } |
| } |
| |
| bool _upgradePubspecs({ |
| required PubDependencyTree tree, |
| required List<PubspecYaml> pubspecs, |
| required Set<String> specialDependencies, |
| required Map<String, PubspecDependency> explicitDependencies, |
| }) { |
| // The transitive dependency tree for the fake package does not contain |
| // dependencies between Flutter SDK packages and pub packages. We add them |
| // here. |
| for (final PubspecYaml pubspec in pubspecs) { |
| final String package = pubspec.name; |
| specialDependencies.add(package); |
| tree._versions[package] = pubspec.version; |
| assert(!tree._dependencyTree.containsKey(package)); |
| tree._dependencyTree[package] = <String>{}; |
| for (final PubspecDependency dependency in pubspec.dependencies) { |
| if (dependency.kind == DependencyKind.normal) { |
| tree._dependencyTree[package]!.add(dependency.name); |
| } |
| } |
| } |
| |
| if (boolArgDeprecated('transitive-closure')) { |
| tree._dependencyTree.forEach((String from, Set<String> to) { |
| globals.printStatus('$from -> $to'); |
| }); |
| return true; |
| } |
| |
| if (boolArgDeprecated('paths')) { |
| showDependencyPaths(from: stringArgDeprecated('from')!, to: stringArgDeprecated('to')!, tree: tree); |
| return true; |
| } |
| |
| // Now that we have collected all the data, we can apply our dependency |
| // versions to each pubspec.yaml that we collected. This mutates the |
| // pubspec.yaml files. |
| // |
| // The specialDependencies argument is the set of package names to not pin |
| // to specific versions because they are explicitly pinned by their |
| // constraints. Here we list the names we earlier established we didn't |
| // need to pin because they come from the Dart or Flutter SDKs. |
| for (final PubspecYaml pubspec in pubspecs) { |
| pubspec.apply(tree, specialDependencies); |
| } |
| return false; |
| } |
| |
| Future<void> _runPubGetOnPackages(List<Directory> packages) async { |
| final Stopwatch timer = Stopwatch()..start(); |
| int count = 0; |
| |
| // Now we run pub get on each of the affected packages to update their |
| // pubspec.lock files with the right transitive dependencies. |
| // |
| // This can be expensive, so we run them in parallel. If we hadn't already |
| // warmed the cache above, running them in parallel could be dangerous due |
| // to contention when unpacking downloaded dependencies, but since we have |
| // downloaded all that we need, it is safe to run them in parallel. |
| final Status status = globals.logger.startProgress( |
| 'Running "flutter pub get" in affected packages...', |
| ); |
| try { |
| // int.tryParse will not accept null, but will convert empty string to null |
| final int? maxJobs = int.tryParse(stringArgDeprecated('jobs') ?? ''); |
| final TaskQueue<void> queue = TaskQueue<void>(maxJobs: maxJobs); |
| for (final Directory dir in packages) { |
| unawaited(queue.add(() async { |
| final Stopwatch stopwatch = Stopwatch(); |
| stopwatch.start(); |
| await pub.get( |
| context: PubContext.updatePackages, |
| project: FlutterProject.fromDirectory(dir), |
| // All dependencies should already have been downloaded by the fake |
| // package, so the concurrent checks can all happen offline. |
| offline: true, |
| printProgress: false, |
| ); |
| stopwatch.stop(); |
| final double seconds = stopwatch.elapsedMilliseconds / 1000.0; |
| final String relativeDir = globals.fs.path.relative(dir.path, from: Cache.flutterRoot); |
| globals.printStatus('Ran pub get in $relativeDir in ${seconds.toStringAsFixed(1)}s...'); |
| })); |
| count += 1; |
| } |
| unawaited(queue.add(() async { |
| final Stopwatch stopwatch = Stopwatch(); |
| await _downloadCoverageData(); |
| stopwatch.stop(); |
| final double seconds = stopwatch.elapsedMilliseconds / 1000.0; |
| globals.printStatus('Downloaded lcov data for package:flutter in ${seconds.toStringAsFixed(1)}s...'); |
| })); |
| await queue.tasksComplete; |
| status.stop(); |
| // The exception is rethrown, so don't catch only Exceptions. |
| } catch (exception) { // ignore: avoid_catches_without_on_clauses |
| status.cancel(); |
| rethrow; |
| } |
| |
| final double seconds = timer.elapsedMilliseconds / 1000.0; |
| globals.printStatus("\nRan 'pub get' $count time${count == 1 ? "" : "s"} and fetched coverage data in ${seconds.toStringAsFixed(1)}s."); |
| } |
| |
| void showDependencyPaths({ |
| required String from, |
| required String to, |
| required PubDependencyTree tree, |
| }) { |
| if (!tree.contains(from)) { |
| throwToolExit('Package $from not found in the dependency tree.'); |
| } |
| if (!tree.contains(to)) { |
| throwToolExit('Package $to not found in the dependency tree.'); |
| } |
| |
| final Queue<_DependencyLink> traversalQueue = Queue<_DependencyLink>(); |
| final Set<String> visited = <String>{}; |
| final List<_DependencyLink> paths = <_DependencyLink>[]; |
| |
| traversalQueue.addFirst(_DependencyLink(from: null, to: from)); |
| while (traversalQueue.isNotEmpty) { |
| final _DependencyLink link = traversalQueue.removeLast(); |
| if (link.to == to) { |
| paths.add(link); |
| } |
| if (link.from != null) { |
| visited.add(link.from!.to); |
| } |
| for (final String dependency in tree._dependencyTree[link.to]!) { |
| if (!visited.contains(dependency)) { |
| traversalQueue.addFirst(_DependencyLink(from: link, to: dependency)); |
| } |
| } |
| } |
| |
| for (_DependencyLink path in paths) { |
| final StringBuffer buf = StringBuffer(); |
| while (path != null) { |
| buf.write(path.to); |
| path = path.from!; |
| if (path != null) { |
| buf.write(' <- '); |
| } |
| } |
| globals.printStatus(buf.toString(), wrap: false); |
| } |
| |
| if (paths.isEmpty) { |
| globals.printStatus('No paths found from $from to $to'); |
| } |
| } |
| } |
| |
| class _DependencyLink { |
| _DependencyLink({ |
| required this.from, |
| required this.to, |
| }); |
| |
| final _DependencyLink? from; |
| final String to; |
| |
| @override |
| String toString() => '${from?.to} -> $to'; |
| } |
| |
| /// The various sections of a pubspec.yaml file. |
| /// |
| /// We care about the "dependencies", "dev_dependencies", and |
| /// "dependency_overrides" sections, as well as the "name" and "version" fields |
| /// in the pubspec header bucketed into [header]. The others are all bucketed |
| /// into [other]. |
| enum Section { header, dependencies, devDependencies, dependencyOverrides, builders, other } |
| |
| /// The various kinds of dependencies we know and care about. |
| enum DependencyKind { |
| // Dependencies that will be path or sdk dependencies but |
| // for which we haven't yet parsed the data. |
| unknown, |
| |
| // Regular dependencies with a specified version range. |
| normal, |
| |
| // Dependency that uses an explicit path, e.g. into the Dart SDK. |
| path, |
| |
| // Dependency defined as coming from an SDK (typically "sdk: flutter"). |
| sdk, |
| |
| // A dependency that was "normal", but for which we later found a "path" or |
| // "sdk" dependency in the dependency_overrides section. |
| overridden, |
| |
| // A dependency that uses git. |
| git, |
| } |
| |
| /// This is the string we output next to each of our autogenerated transitive |
| /// dependencies so that we can ignore them the next time we parse the |
| /// pubspec.yaml file. |
| const String kTransitiveMagicString= '# THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade"'; |
| |
| |
| /// This is the string output before a checksum of the packages used. |
| const String kDependencyChecksum = '# PUBSPEC CHECKSUM: '; |
| |
| /// This class represents a pubspec.yaml file for the purposes of upgrading the |
| /// dependencies as done by this file. |
| class PubspecYaml { |
| /// You create one of these by providing a directory, from which we obtain the |
| /// pubspec.yaml and parse it into a line-by-line form. |
| factory PubspecYaml(Directory directory) { |
| final File file = _pubspecFor(directory); |
| return _parse(file, file.readAsLinesSync()); |
| } |
| |
| PubspecYaml._(this.file, this.name, this.version, this.inputData, this.checksum); |
| |
| final File file; // The actual pubspec.yaml file. |
| |
| /// The package name. |
| final String name; |
| |
| /// The package version. |
| final String? version; |
| |
| final List<PubspecLine> inputData; // Each line of the pubspec.yaml file, parsed(ish). |
| |
| /// The package checksum. |
| /// |
| /// If this was not found in the pubspec, a synthetic checksum is created |
| /// with a value of `-1`. |
| final PubspecChecksum checksum; |
| |
| /// This parses each line of a pubspec.yaml file (a list of lines) into |
| /// slightly more structured data (in the form of a list of PubspecLine |
| /// objects). We don't just use a YAML parser because we care about comments |
| /// and also because we can just define the style of pubspec.yaml files we care |
| /// about (since they're all under our control). |
| static PubspecYaml _parse(File file, List<String> lines) { |
| final String filename = file.path; |
| String? packageName; |
| String? packageVersion; |
| PubspecChecksum? checksum; // the checksum value used to verify that dependencies haven't changed. |
| final List<PubspecLine> result = <PubspecLine>[]; // The output buffer. |
| Section section = Section.other; // Which section we're currently reading from. |
| bool seenMain = false; // Whether we've seen the "dependencies:" section. |
| bool seenDev = false; // Whether we've seen the "dev_dependencies:" section. |
| // The masterDependencies map is used to keep track of the objects |
| // representing actual dependencies we've seen so far in this file so that |
| // if we see dependency overrides we can update the actual dependency so it |
| // knows that it's not really a dependency. |
| final Map<String, PubspecDependency> masterDependencies = <String, PubspecDependency>{}; |
| // The "special" dependencies (the ones that use git: or path: or sdk: or |
| // whatnot) have the style of having extra data after the line that declares |
| // the dependency. So we track what is the "current" (or "last") dependency |
| // that we are dealing with using this variable. |
| PubspecDependency? lastDependency; |
| for (int index = 0; index < lines.length; index += 1) { |
| String line = lines[index]; |
| if (lastDependency == null) { |
| // First we look to see if we're transitioning to a new top-level section. |
| // The PubspecHeader.parse static method can recognize those headers. |
| final PubspecHeader? header = PubspecHeader.parse(line); // See if it's a header. |
| if (header != null) { // It is! |
| section = header.section; // The parser determined what kind of section it is. |
| if (section == Section.header) { |
| if (header.name == 'name') { |
| packageName = header.value; |
| } else if (header.name == 'version') { |
| packageVersion = header.value; |
| } |
| } else if (section == Section.dependencies) { |
| // If we're entering the "dependencies" section, we want to make sure that |
| // it's the first section (of those we care about) that we've seen so far. |
| if (seenMain) { |
| throwToolExit('Two dependencies sections found in $filename. There should only be one.'); |
| } |
| if (seenDev) { |
| throwToolExit('The dependencies section was after the dev_dependencies section in $filename. ' |
| 'To enable one-pass processing, the dependencies section must come before the ' |
| 'dev_dependencies section.'); |
| } |
| seenMain = true; |
| } else if (section == Section.devDependencies) { |
| // Similarly, if we're entering the dev_dependencies section, we should verify |
| // that we've not seen one already. |
| if (seenDev) { |
| throwToolExit('Two dev_dependencies sections found in $filename. There should only be one.'); |
| } |
| seenDev = true; |
| } |
| result.add(header); |
| } else if (section == Section.builders) { |
| // Do nothing. |
| // This line isn't a section header, and we're not in a section we care about. |
| // We just stick the line into the output unmodified. |
| result.add(PubspecLine(line)); |
| } else if (section == Section.other) { |
| if (line.contains(kDependencyChecksum)) { |
| // This is the pubspec checksum. After computing it, we remove it from the output data |
| // since it will be recomputed later. |
| checksum = PubspecChecksum.parse(line); |
| } else { |
| // This line isn't a section header, and we're not in a section we care about. |
| // We just stick the line into the output unmodified. |
| result.add(PubspecLine(line)); |
| } |
| } else { |
| // We're in a section we care about. Try to parse out the dependency: |
| final PubspecDependency? dependency = PubspecDependency.parse(line, filename: filename); |
| if (dependency != null) { // We got one! |
| // Track whether or not this a dev dependency. |
| dependency.isDevDependency = seenDev; |
| result.add(dependency); |
| if (dependency.kind == DependencyKind.unknown) { |
| // If we didn't get a version number, then we need to be ready to |
| // read the next line as part of this dependency, so keep track of |
| // this dependency object. |
| lastDependency = dependency; |
| } |
| if (section != Section.dependencyOverrides) { |
| // If we're not in the overrides section, then just remember the |
| // dependency, in case it comes up again later in the overrides |
| // section. |
| // |
| // First, make sure it's a unique dependency. Listing dependencies |
| // twice doesn't make sense. |
| if (masterDependencies.containsKey(dependency.name)) { |
| throwToolExit('$filename contains two dependencies on ${dependency.name}.'); |
| } |
| masterDependencies[dependency.name] = dependency; |
| } else { |
| // If we _are_ in the overrides section, then go tell the version |
| // we saw earlier (if any -- there might not be, we might be |
| // overriding a transitive dependency) that we have overridden it, |
| // so that later when we output the dependencies we can leave |
| // the line unmodified. |
| masterDependencies[dependency.name]?.markOverridden(dependency); |
| } |
| } else if (line.contains(kDependencyChecksum)) { |
| // This is the pubspec checksum. After computing it, we remove it from the output data |
| // since it will be recomputed later. |
| checksum = PubspecChecksum.parse(line); |
| } else { |
| // We're in a section we care about but got a line we didn't |
| // recognize. Maybe it's a comment or a blank line or something. |
| // Just pass it through. |
| result.add(PubspecLine(line)); |
| } |
| } |
| } else { |
| // If we're here it means the last line was a dependency that needed |
| // extra information to be parsed from the next line. |
| // |
| // Try to parse the line by giving it to the last PubspecDependency |
| // object we created. If parseLock fails to recognize the line, it will |
| // throw. If it does recognize the line and needs the following lines in |
| // its lockLine, it'll return false. |
| // Otherwise it returns true. |
| // |
| // If it returns true, then it will have updated itself internally to |
| // store the information from this line. |
| if (!lastDependency.parseLock(line, filename, lockIsOverride: section == Section.dependencyOverrides)) { |
| // Ok we're dealing with some "git:" dependency. Consume lines until |
| // we are out of the git dependency, and stuff them into the lock |
| // line. |
| lastDependency._lockLine = line; |
| lastDependency._lockIsOverride = section == Section.dependencyOverrides; |
| do { |
| index += 1; |
| if (index == lines.length) { |
| throw StateError('Invalid pubspec.yaml: a "git" dependency section terminated early.'); |
| } |
| line = lines[index]; |
| lastDependency._lockLine = '${lastDependency._lockLine}\n$line'; |
| } while (line.startsWith(' ')); |
| } |
| // We're done with this special dependency, so reset back to null so |
| // we'll go in the top section next time instead. |
| lastDependency = null; |
| } |
| } |
| return PubspecYaml._(file, packageName!, packageVersion, result, checksum ?? PubspecChecksum(null, '')); |
| } |
| |
| /// This returns all the explicit dependencies that this pubspec.yaml lists under dependencies. |
| Iterable<PubspecDependency> get dependencies { |
| // It works by iterating over the parsed data from _parse above, collecting |
| // all the dependencies that were found, ignoring any that are flagged as as |
| // overridden by subsequent entries in the same file and any that have the |
| // magic comment flagging them as auto-generated transitive dependencies |
| // that we added in a previous run. |
| return inputData |
| .whereType<PubspecDependency>() |
| .where((PubspecDependency data) => data.kind != DependencyKind.overridden && !data.isTransitive && !data.isDevDependency); |
| } |
| |
| /// This returns all regular dependencies and all dev dependencies. |
| Iterable<PubspecDependency> get allExplicitDependencies { |
| return inputData |
| .whereType<PubspecDependency>() |
| .where((PubspecDependency data) => data.kind != DependencyKind.overridden && !data.isTransitive); |
| } |
| |
| /// This returns all dependencies. |
| Iterable<PubspecDependency> get allDependencies { |
| return inputData.whereType<PubspecDependency>(); |
| } |
| |
| /// Take a dependency graph with explicit version numbers, and apply them to |
| /// the pubspec.yaml, ignoring any that we know are special dependencies (those |
| /// that depend on the Flutter or Dart SDK directly and are thus automatically |
| /// pinned). |
| void apply(PubDependencyTree versions, Set<String> specialDependencies) { |
| assert(versions != null); |
| final List<String> output = <String>[]; // the string data to output to the file, line by line |
| final Set<String> directDependencies = <String>{}; // packages this pubspec directly depends on (i.e. not transitive) |
| final Set<String> devDependencies = <String>{}; |
| Section section = Section.other; // the section we're currently handling |
| |
| // the line number where we're going to insert the transitive dependencies. |
| int? endOfDirectDependencies; |
| // The line number where we're going to insert the transitive dev dependencies. |
| int? endOfDevDependencies; |
| // Walk the pre-parsed input file, outputting it unmodified except for |
| // updating version numbers, removing the old transitive dependencies lines, |
| // and adding our new transitive dependencies lines. We also do a little |
| // cleanup, removing trailing spaces, removing double-blank lines, leading |
| // blank lines, and trailing blank lines, and ensuring the file ends with a |
| // newline. This cleanup lets us be a little more aggressive while building |
| // the output. |
| for (final PubspecLine data in inputData) { |
| if (data is PubspecHeader) { |
| // This line was a header of some sort. |
| // |
| // If we're leaving one of the sections in which we can list transitive |
| // dependencies, then remember this as the current last known valid |
| // place to insert our transitive dependencies. |
| if (section == Section.dependencies) { |
| endOfDirectDependencies = output.length; |
| } |
| if (section == Section.devDependencies) { |
| endOfDevDependencies = output.length; |
| } |
| section = data.section; // track which section we're now in. |
| output.add(data.line); // insert the header into the output |
| } else if (data is PubspecDependency) { |
| // This was a dependency of some sort. |
| // How we handle this depends on the section. |
| switch (section) { |
| case Section.devDependencies: |
| case Section.dependencies: |
| // For the dependencies and dev_dependencies sections, we reinsert |
| // the dependency if it wasn't one of our autogenerated transitive |
| // dependency lines. |
| if (!data.isTransitive) { |
| // Assert that we haven't seen it in this file already. |
| assert(!directDependencies.contains(data.name) && !devDependencies.contains(data.name)); |
| if (data.kind == DependencyKind.normal) { |
| // This is a regular dependency, so we need to update the |
| // version number. |
| // |
| // We output data that matches the format that |
| // PubspecDependency.parse can handle. The data.suffix is any |
| // previously-specified trailing comment. |
| assert(versions.contains(data.name), |
| "versions doesn't contain ${data.name}"); |
| output.add(' ${data.name}: ${versions.versionFor(data.name)}${data.suffix}'); |
| } else { |
| // If it wasn't a regular dependency, then we output the line |
| // unmodified. If there was an additional line (e.g. an "sdk: |
| // flutter" line) then we output that too. |
| output.add(data.line); |
| if (data.lockLine != null) { |
| output.add(data.lockLine!); |
| } |
| } |
| // Remember that we've dealt with this dependency so we don't |
| // mention it again when doing the transitive dependencies. |
| if (section == Section.dependencies) { |
| directDependencies.add(data.name); |
| } else { |
| devDependencies.add(data.name); |
| } |
| } |
| // Since we're in one of the places where we can list dependencies, |
| // remember this as the current last known valid place to insert our |
| // transitive dev dependencies. If the section is for regular dependencies, |
| // then also remember the line for the end of direct dependencies. |
| if (section == Section.dependencies) { |
| endOfDirectDependencies = output.length; |
| } |
| endOfDevDependencies = output.length; |
| break; |
| case Section.builders: |
| case Section.dependencyOverrides: |
| case Section.header: |
| case Section.other: |
| // In other sections, pass everything through in its original form. |
| output.add(data.line); |
| if (data.lockLine != null) { |
| output.add(data.lockLine!); |
| } |
| break; |
| } |
| } else { |
| // Not a header, not a dependency, just pass that through unmodified. |
| output.add(data.line); |
| } |
| } |
| |
| // If there are no dependencies or dev_dependencies sections, these will be |
| // null. We have such files in our tests, so account for them here. |
| endOfDirectDependencies ??= output.length; |
| endOfDevDependencies ??= output.length; |
| |
| // Now include all the transitive dependencies and transitive dev dependencies. |
| // The blocks of text to insert for each dependency section. |
| final List<String> transitiveDependencyOutput = <String>[]; |
| final List<String> transitiveDevDependencyOutput = <String>[]; |
| |
| // Which dependencies we need to handle for the transitive and dev dependency sections. |
| final Set<String> transitiveDependencies = <String>{}; |
| final Set<String> transitiveDevDependencies = <String>{}; |
| |
| // Merge the lists of dependencies we've seen in this file from dependencies, dev dependencies, |
| // and the dependencies we know this file mentions that are already pinned |
| // (and which didn't get special processing above). |
| final Set<String> implied = <String>{ |
| ...directDependencies, |
| ...specialDependencies, |
| ...devDependencies, |
| }; |
| |
| // Create a new set to hold the list of packages we've already processed, so |
| // that we don't redundantly process them multiple times. |
| final Set<String> done = <String>{}; |
| for (final String package in directDependencies) { |
| transitiveDependencies.addAll(versions.getTransitiveDependenciesFor(package, seen: done, exclude: implied)); |
| } |
| for (final String package in devDependencies) { |
| transitiveDevDependencies.addAll(versions.getTransitiveDependenciesFor(package, seen: done, exclude: implied)); |
| } |
| |
| // Sort each dependency block lexically so that we don't get noisy diffs when upgrading. |
| final List<String> transitiveDependenciesAsList = transitiveDependencies.toList()..sort(); |
| final List<String> transitiveDevDependenciesAsList = transitiveDevDependencies.toList()..sort(); |
| |
| String computeTransitiveDependencyLineFor(String package) { |
| return ' $package: ${versions.versionFor(package)} $kTransitiveMagicString'; |
| } |
| |
| // Add a line for each transitive dependency and transitive dev dependency using our magic string to recognize them later. |
| for (final String package in transitiveDependenciesAsList) { |
| transitiveDependencyOutput.add(computeTransitiveDependencyLineFor(package)); |
| } |
| for (final String package in transitiveDevDependenciesAsList) { |
| transitiveDevDependencyOutput.add(computeTransitiveDependencyLineFor(package)); |
| } |
| |
| // Build a sorted list of all dependencies for the checksum. |
| final Set<String> checksumDependencies = <String>{ |
| ...directDependencies, |
| ...devDependencies, |
| ...transitiveDependenciesAsList, |
| ...transitiveDevDependenciesAsList, |
| }..removeAll(specialDependencies); |
| |
| // Add a blank line before and after each section to keep the resulting output clean. |
| transitiveDependencyOutput |
| ..insert(0, '') |
| ..add(''); |
| transitiveDevDependencyOutput |
| ..insert(0, '') |
| ..add(''); |
| |
| // Compute a new checksum from all sorted dependencies and their version and convert to a hex string. |
| final String checksumString = _computeChecksum(checksumDependencies, versions.versionFor); |
| |
| // Insert the block of transitive dependency declarations into the output after [endOfDirectDependencies], |
| // and the blocks of transitive dev dependency declarations into the output after [lastPossiblePlace]. Finally, |
| // insert the [checksumString] at the very end. |
| output |
| ..insertAll(endOfDevDependencies, transitiveDevDependencyOutput) |
| ..insertAll(endOfDirectDependencies, transitiveDependencyOutput) |
| ..add('') |
| ..add('$kDependencyChecksum$checksumString'); |
| |
| // Remove trailing lines. |
| while (output.last.isEmpty) { |
| output.removeLast(); |
| } |
| |
| // Output the result to the pubspec.yaml file, skipping leading and |
| // duplicate blank lines and removing trailing spaces. |
| final StringBuffer contents = StringBuffer(); |
| bool hadBlankLine = true; |
| for (String line in output) { |
| line = line.trimRight(); |
| if (line == '') { |
| if (!hadBlankLine) { |
| contents.writeln(); |
| } |
| hadBlankLine = true; |
| } else { |
| contents.writeln(line); |
| hadBlankLine = false; |
| } |
| } |
| file.writeAsStringSync(contents.toString()); |
| } |
| } |
| |
| /// This is the base class for the objects that represent lines in the |
| /// pubspec.yaml files. |
| class PubspecLine { |
| PubspecLine(this.line); |
| |
| /// The raw line as we saw it in the original file. This is used so that we can |
| /// output the same line unmodified for the majority of lines. |
| final String line; |
| } |
| |
| /// A checksum of the non autogenerated dependencies. |
| class PubspecChecksum extends PubspecLine { |
| PubspecChecksum(this.value, String line) : super(line); |
| |
| /// The checksum value, computed using [Object.hash] over the direct, dev, |
| /// and special dependencies sorted lexically. |
| /// |
| /// If the line cannot be parsed, [value] will be null. |
| final String? value; |
| |
| /// Parses a [PubspecChecksum] from a line. |
| /// |
| /// The returned PubspecChecksum will have a null [value] if no checksum could |
| /// be found on this line. This is a value that [_computeChecksum] cannot return. |
| static PubspecChecksum parse(String line) { |
| final List<String> tokens = line.split(kDependencyChecksum); |
| if (tokens.length != 2) { |
| return PubspecChecksum(null, line); |
| } |
| return PubspecChecksum(tokens.last.trim(), line); |
| } |
| } |
| |
| /// A header, e.g. "dependencies:". |
| class PubspecHeader extends PubspecLine { |
| PubspecHeader( |
| super.line, |
| this.section, { |
| this.name, |
| this.value, |
| }); |
| |
| /// The section of the pubspec where the parse [line] appears. |
| final Section section; |
| |
| /// The name in the pubspec line providing a name/value pair, such as "name" |
| /// and "version". |
| /// |
| /// Example: |
| /// |
| /// The value of this field extracted from the following line is "version". |
| /// |
| /// ``` |
| /// version: 0.16.5 |
| /// ``` |
| final String? name; |
| |
| /// The value in the pubspec line providing a name/value pair, such as "name" |
| /// and "version". |
| /// |
| /// Example: |
| /// |
| /// The value of this field extracted from the following line is "0.16.5". |
| /// |
| /// ``` |
| /// version: 0.16.5 |
| /// ``` |
| final String? value; |
| |
| static PubspecHeader? parse(String line) { |
| // We recognize any line that: |
| // * doesn't start with a space (i.e. is aligned on the left edge) |
| // * ignoring trailing spaces and comments, ends with a colon |
| // * has contents before the colon |
| // We also try to recognize which of the kinds of Sections it is |
| // by comparing those contents against known strings. |
| if (line.startsWith(' ')) { |
| return null; |
| } |
| final String strippedLine = _stripComments(line); |
| if (!strippedLine.contains(':') || strippedLine.length <= 1) { |
| return null; |
| } |
| final List<String> parts = strippedLine.split(':'); |
| final String sectionName = parts.first; |
| final String value = parts.last.trim(); |
| switch (sectionName) { |
| case 'dependencies': |
| return PubspecHeader(line, Section.dependencies); |
| case 'dev_dependencies': |
| return PubspecHeader(line, Section.devDependencies); |
| case 'dependency_overrides': |
| return PubspecHeader(line, Section.dependencyOverrides); |
| case 'builders': |
| return PubspecHeader(line, Section.builders); |
| case 'name': |
| case 'version': |
| return PubspecHeader(line, Section.header, name: sectionName, value: value); |
| default: |
| return PubspecHeader(line, Section.other); |
| } |
| } |
| |
| /// Returns the input after removing trailing spaces and anything after the |
| /// first "#". |
| static String _stripComments(String line) { |
| final int hashIndex = line.indexOf('#'); |
| if (hashIndex < 0) { |
| return line.trimRight(); |
| } |
| return line.substring(0, hashIndex).trimRight(); |
| } |
| } |
| |
| /// A dependency, as represented by a line (or two) from a pubspec.yaml file. |
| class PubspecDependency extends PubspecLine { |
| PubspecDependency( |
| super.line, |
| this.name, |
| this.suffix, { |
| required this.isTransitive, |
| required DependencyKind kind, |
| required this.version, |
| required this.sourcePath, |
| }) : _kind = kind; |
| |
| static PubspecDependency? parse(String line, { required String filename }) { |
| // We recognize any line that: |
| // * starts with exactly two spaces, no more or less |
| // * has some content, then a colon |
| // |
| // If we recognize the line, then we look to see if there's anything after |
| // the colon, ignoring comments. If there is, then this is a normal |
| // dependency, otherwise it's an unknown one. |
| // |
| // We also try and save the version string, if any. This is used to verify |
| // the checksum of package deps. |
| // |
| // We also look at the trailing comment, if any, to see if it is the magic |
| // string that identifies the line as a transitive dependency that we |
| // previously pinned, so we can ignore it. |
| // |
| // We remember the trailing comment, if any, so that we can reconstruct the |
| // line later. We forget the specified version range, if any. |
| if (line.length < 4 || line.startsWith(' ') || !line.startsWith(' ')) { |
| return null; |
| } |
| final int colonIndex = line.indexOf(':'); |
| final int hashIndex = line.indexOf('#'); |
| if (colonIndex < 3) { // two spaces at 0 and 1, a character at 2 |
| return null; |
| } |
| if (hashIndex >= 0 && hashIndex < colonIndex) { |
| return null; |
| } |
| final String package = line.substring(2, colonIndex).trimRight(); |
| assert(package.isNotEmpty); |
| assert(line.startsWith(' $package')); |
| String suffix = ''; |
| bool isTransitive = false; |
| String stripped; |
| String version = ''; |
| if (hashIndex >= 0) { |
| assert(hashIndex > colonIndex); |
| final String trailingComment = line.substring(hashIndex, line.length); |
| assert(line.endsWith(trailingComment)); |
| isTransitive = trailingComment == kTransitiveMagicString; |
| suffix = ' $trailingComment'; |
| stripped = line.substring(colonIndex + 1, hashIndex).trimRight(); |
| } else { |
| stripped = line.substring(colonIndex + 1, line.length).trimRight(); |
| } |
| if (colonIndex != -1) { |
| version = line.substring(colonIndex + 1, hashIndex != -1 ? hashIndex : line.length).trim(); |
| } |
| return PubspecDependency(line, package, suffix, isTransitive: isTransitive, version: version, kind: stripped.isEmpty ? DependencyKind.unknown : DependencyKind.normal, sourcePath: filename); |
| } |
| |
| final String name; // the package name |
| final String suffix; // any trailing comment we found |
| final String version; // the version string if found, or blank. |
| final bool isTransitive; // whether the suffix matched kTransitiveMagicString |
| final String sourcePath; // the filename of the pubspec.yaml file, for error messages |
| late bool isDevDependency; // Whether this dependency is under the `dev dependencies` section. |
| |
| DependencyKind get kind => _kind; |
| DependencyKind _kind = DependencyKind.normal; |
| |
| /// If we're a path or sdk dependency, the path or sdk in question. |
| String? _lockTarget; |
| |
| /// If we were a two-line dependency, the second line (see the inherited [line] |
| /// for the first). |
| String? get lockLine => _lockLine; |
| String? _lockLine; |
| |
| /// If we're a path or sdk dependency, whether we were found in a |
| /// dependencies/dev_dependencies section, or a dependency_overrides section. |
| /// We track this so that we can put ourselves in the right section when |
| /// generating the fake pubspec.yaml. |
| bool get lockIsOverride => _lockIsOverride; |
| late bool _lockIsOverride; |
| |
| static const String _pathPrefix = ' path: '; |
| static const String _sdkPrefix = ' sdk: '; |
| static const String _gitPrefix = ' git:'; |
| |
| /// Whether the dependency points to a package in the Flutter SDK. |
| /// |
| /// There are two ways one can point to a Flutter package: |
| /// |
| /// - Using a "sdk: flutter" dependency. |
| /// - Using a "path" dependency that points somewhere in the Flutter |
| /// repository other than the "bin" directory. |
| bool get pointsToSdk { |
| if (_kind == DependencyKind.sdk) { |
| return true; |
| } |
| |
| final String? lockTarget = _lockTarget; |
| if (_kind == DependencyKind.path && lockTarget != null && |
| !globals.fs.path.isWithin(globals.fs.path.join(Cache.flutterRoot!, 'bin'), lockTarget) && |
| globals.fs.path.isWithin(Cache.flutterRoot!, lockTarget)) { |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /// If parse decided we were a two-line dependency, this is called to parse the second line. |
| /// We throw if we couldn't parse this line. |
| /// We return true if we parsed it and stored the line in lockLine. |
| /// We return false if we parsed it and it's a git dependency that needs the next few lines. |
| bool parseLock(String line, String pubspecPath, { required bool lockIsOverride }) { |
| assert(lockIsOverride != null); |
| assert(kind == DependencyKind.unknown); |
| if (line.startsWith(_pathPrefix)) { |
| // We're a path dependency; remember the (absolute) path. |
| _lockTarget = globals.fs.path.canonicalize( |
| globals.fs.path.absolute(globals.fs.path.dirname(pubspecPath), line.substring(_pathPrefix.length, line.length)) |
| ); |
| _kind = DependencyKind.path; |
| } else if (line.startsWith(_sdkPrefix)) { |
| // We're an SDK dependency. |
| _lockTarget = line.substring(_sdkPrefix.length, line.length); |
| _kind = DependencyKind.sdk; |
| } else if (line.startsWith(_gitPrefix)) { |
| // We're a git: dependency. We'll have to get the next few lines. |
| _kind = DependencyKind.git; |
| return false; |
| } else { |
| throwToolExit('Could not parse additional details for dependency $name; line was: "$line"'); |
| } |
| _lockIsOverride = lockIsOverride; |
| _lockLine = line; |
| return true; |
| } |
| |
| void markOverridden(PubspecDependency sibling) { |
| // This is called when we find a dependency is mentioned a second time, |
| // first in dependencies/dev_dependencies, and then in dependency_overrides. |
| // It is called on the one found in dependencies/dev_dependencies, so that |
| // we'll later know to report our version as "any" in the fake pubspec.yaml |
| // and unmodified in the official pubspec.yamls. |
| assert(sibling.name == name); |
| assert(sibling.sourcePath == sourcePath); |
| assert(sibling.kind != DependencyKind.normal); |
| _kind = DependencyKind.overridden; |
| } |
| |
| /// This generates the entry for this dependency for the pubspec.yaml for the |
| /// fake package that we'll use to get the version numbers figured out. |
| /// |
| /// When called with [doUpgrade] as [true], the version constrains will be set |
| /// to >= whatever the previous version was. If [doUpgrade] is [false], then |
| /// the previous version is used again as an exact pin. |
| void describeForFakePubspec(StringBuffer dependencies, StringBuffer overrides, { bool doUpgrade = true }) { |
| final String versionToUse; |
| // This should only happen when manually adding new dependencies; otherwise |
| // versions should always be pinned exactly |
| if (version.isEmpty || version == 'any') { |
| versionToUse = 'any'; |
| } else if (doUpgrade) { |
| // Must wrap in quotes for Yaml parsing |
| versionToUse = "'>= $version'"; |
| } else { |
| versionToUse = version; |
| } |
| switch (kind) { |
| case DependencyKind.unknown: |
| case DependencyKind.overridden: |
| assert(kind != DependencyKind.unknown); |
| break; |
| case DependencyKind.normal: |
| if (!kManuallyPinnedDependencies.containsKey(name)) { |
| dependencies.writeln(' $name: $versionToUse'); |
| } |
| break; |
| case DependencyKind.path: |
| if (lockIsOverride) { |
| dependencies.writeln(' $name: $versionToUse'); |
| overrides.writeln(' $name:'); |
| overrides.writeln(' path: $_lockTarget'); |
| } else { |
| dependencies.writeln(' $name:'); |
| dependencies.writeln(' path: $_lockTarget'); |
| } |
| break; |
| case DependencyKind.sdk: |
| if (lockIsOverride) { |
| dependencies.writeln(' $name: $versionToUse'); |
| overrides.writeln(' $name:'); |
| overrides.writeln(' sdk: $_lockTarget'); |
| } else { |
| dependencies.writeln(' $name:'); |
| dependencies.writeln(' sdk: $_lockTarget'); |
| } |
| break; |
| case DependencyKind.git: |
| if (lockIsOverride) { |
| dependencies.writeln(' $name: $versionToUse'); |
| overrides.writeln(' $name:'); |
| overrides.writeln(lockLine); |
| } else { |
| dependencies.writeln(' $name:'); |
| dependencies.writeln(lockLine); |
| } |
| break; |
| } |
| } |
| |
| @override |
| String toString() { |
| return '$name: $version'; |
| } |
| } |
| |
| /// Generates the File object for the pubspec.yaml file of a given Directory. |
| File _pubspecFor(Directory directory) { |
| return directory.fileSystem.file( |
| directory.fileSystem.path.join(directory.path, 'pubspec.yaml')); |
| } |
| |
| /// Generates the source of a fake pubspec.yaml file given a list of |
| /// dependencies. |
| @visibleForTesting |
| String generateFakePubspec( |
| Iterable<PubspecDependency> dependencies, { |
| bool doUpgrade = false |
| }) { |
| final StringBuffer result = StringBuffer(); |
| final StringBuffer overrides = StringBuffer(); |
| final bool verbose = doUpgrade; |
| result.writeln('name: flutter_update_packages'); |
| result.writeln('environment:'); |
| result.writeln(" sdk: '>=2.10.0 <4.0.0'"); |
| result.writeln('dependencies:'); |
| overrides.writeln('dependency_overrides:'); |
| if (kManuallyPinnedDependencies.isNotEmpty) { |
| if (verbose) { |
| globals.printStatus('WARNING: the following packages use hard-coded version constraints:'); |
| } |
| final Set<String> allTransitive = <String>{ |
| for (final PubspecDependency dependency in dependencies) |
| dependency.name, |
| }; |
| kManuallyPinnedDependencies.forEach((String package, String version) { |
| // Don't add pinned dependency if it is not in the set of all transitive dependencies. |
| if (!allTransitive.contains(package)) { |
| if (verbose) { |
| globals.printStatus('Skipping $package because it was not transitive'); |
| } |
| return; |
| } |
| result.writeln(' $package: $version'); |
| if (verbose) { |
| globals.printStatus(' - $package: $version'); |
| } |
| }); |
| } |
| for (final PubspecDependency dependency in dependencies) { |
| if (!dependency.pointsToSdk) { |
| dependency.describeForFakePubspec(result, overrides, doUpgrade: doUpgrade); |
| } |
| } |
| result.write(overrides.toString()); |
| return result.toString(); |
| } |
| |
| /// This object tracks the output of a call to "pub deps --style=compact". |
| /// |
| /// It ends up holding the full graph of dependencies, and the version number for |
| /// each one. |
| class PubDependencyTree { |
| final Map<String, String?> _versions = <String, String?>{}; |
| final Map<String, Set<String>> _dependencyTree = <String, Set<String>>{}; |
| |
| /// Handles the output from "pub deps --style=compact". |
| /// |
| /// That output is of this form: |
| /// |
| /// ``` |
| /// package_name 0.0.0 |
| /// |
| /// dependencies: |
| /// - analyzer 0.31.0-alpha.0 [watcher args package_config collection] |
| /// - archive 1.0.31 [crypto args path] |
| /// - args 0.13.7 |
| /// - cli_util 0.1.2+1 [path] |
| /// |
| /// dev dependencies: |
| /// - async 1.13.3 [collection] |
| /// - barback 0.15.2+11 [stack_trace source_span pool async collection path] |
| /// |
| /// dependency overrides: |
| /// - analyzer 0.31.0-alpha.0 [watcher args package_config collection] |
| /// ``` |
| /// |
| /// We ignore all the lines that don't start with a hyphen. For each other |
| /// line, we ignore any line that mentions a package we've already seen (this |
| /// happens when the overrides section mentions something that was in the |
| /// dependencies section). We ignore if something is a dependency or |
| /// dev_dependency (pub won't use different versions for those two). |
| /// |
| /// We then parse out the package name, version number, and sub-dependencies for |
| /// each entry, and store than in our _versions and _dependencyTree fields |
| /// above. |
| String? fill(String message) { |
| if (message.startsWith('- ')) { |
| final int space2 = message.indexOf(' ', 2); |
| int space3 = message.indexOf(' ', space2 + 1); |
| if (space3 < 0) { |
| space3 = message.length; |
| } |
| final String package = message.substring(2, space2); |
| if (!contains(package)) { |
| // Some packages get listed in the dependency overrides section too. |
| // We just ignore those. The data is the same either way. |
| final String version = message.substring(space2 + 1, space3); |
| List<String> dependencies; |
| if (space3 < message.length) { |
| assert(message[space3 + 1] == '['); |
| assert(message[message.length - 1] == ']'); |
| final String allDependencies = message.substring(space3 + 2, message.length - 1); |
| dependencies = allDependencies.split(' '); |
| } else { |
| dependencies = const <String>[]; |
| } |
| _versions[package] = version; |
| _dependencyTree[package] = Set<String>.of(dependencies); |
| } |
| } |
| return null; |
| } |
| |
| /// Whether we know about this package. |
| bool contains(String package) { |
| return _versions.containsKey(package); |
| } |
| |
| /// The transitive closure of all the dependencies for the given package, |
| /// excluding any listed in `seen`. |
| Iterable<String> getTransitiveDependenciesFor( |
| String package, { |
| required Set<String> seen, |
| required Set<String> exclude, |
| List<String>? result, |
| }) { |
| assert(seen != null); |
| assert(exclude != null); |
| result ??= <String>[]; |
| final Set<String>? dependencies = _dependencyTree[package]; |
| if (dependencies == null) { |
| // We have no transitive dependencies extracted for flutter_sdk packages |
| // because they were omitted from pubspec.yaml used for 'pub upgrade' run. |
| return result; |
| } |
| for (final String dependency in dependencies) { |
| if (!seen.contains(dependency)) { |
| if (!exclude.contains(dependency)) { |
| result.add(dependency); |
| } |
| seen.add(dependency); |
| getTransitiveDependenciesFor(dependency, seen: seen, exclude: exclude, result: result); |
| } |
| } |
| return result; |
| } |
| |
| /// The version that a particular package ended up with. |
| String versionFor(String package) { |
| return _versions[package]!; |
| } |
| } |
| |
| // Produces a 16-bit checksum from the codePoints of the package name and |
| // version strings using Fletcher's algorithm. |
| String _computeChecksum(Iterable<String> names, String Function(String name) getVersion) { |
| int lowerCheck = 0; |
| int upperCheck = 0; |
| final List<String> sortedNames = names.toList()..sort(); |
| for (final String name in sortedNames) { |
| final String version = getVersion(name); |
| if (version == null) { |
| continue; |
| } |
| final String value = '$name: $version'; |
| // Each code unit is 16 bits. |
| for (final int codeUnit in value.codeUnits) { |
| final int upper = codeUnit >> 8; |
| final int lower = codeUnit & 0xFF; |
| lowerCheck = (lowerCheck + upper) % 255; |
| upperCheck = (upperCheck + lowerCheck) % 255; |
| lowerCheck = (lowerCheck + lower) % 255; |
| upperCheck = (upperCheck + lowerCheck) % 255; |
| } |
| } |
| return ((upperCheck << 8) | lowerCheck).toRadixString(16).padLeft(4, '0'); |
| } |
| |
| /// Create a synthetic Flutter SDK so that pub version solving does not get |
| /// stuck on the old versions. |
| @visibleForTesting |
| Directory createTemporaryFlutterSdk( |
| Logger logger, |
| FileSystem fileSystem, |
| Directory realFlutter, |
| List<PubspecYaml> pubspecs, |
| Directory tempDir, |
| ) { |
| final Set<String> currentPackages = <String>{}; |
| for (final FileSystemEntity entity in realFlutter.childDirectory('packages').listSync()) { |
| // Verify that a pubspec.yaml exists to ensure this isn't a left over directory. |
| if (entity is Directory && entity.childFile('pubspec.yaml').existsSync()) { |
| currentPackages.add(fileSystem.path.basename(entity.path)); |
| } |
| } |
| |
| final Map<String, PubspecYaml> pubspecsByName = <String, PubspecYaml>{}; |
| for (final PubspecYaml pubspec in pubspecs) { |
| pubspecsByName[pubspec.name] = pubspec; |
| } |
| |
| final Directory directory = tempDir.childDirectory('flutter_upgrade_sdk') |
| ..createSync(); |
| // Fill in version info. |
| realFlutter.childFile('version') |
| .copySync(directory.childFile('version').path); |
| |
| // Directory structure should mirror the current Flutter SDK |
| final Directory packages = directory.childDirectory('packages'); |
| for (final String flutterPackage in currentPackages) { |
| final File pubspecFile = packages |
| .childDirectory(flutterPackage) |
| .childFile('pubspec.yaml') |
| ..createSync(recursive: true); |
| final PubspecYaml? pubspecYaml = pubspecsByName[flutterPackage]; |
| if (pubspecYaml == null) { |
| logger.printWarning( |
| "Unexpected package '$flutterPackage' found in packages directory", |
| ); |
| continue; |
| } |
| final StringBuffer output = StringBuffer('name: $flutterPackage\n'); |
| |
| // Fill in SDK dependency constraint. |
| output.write(''' |
| environment: |
| sdk: ">=2.7.0 <4.0.0" |
| '''); |
| |
| output.writeln('dependencies:'); |
| for (final PubspecDependency dependency in pubspecYaml.dependencies) { |
| if (dependency.isTransitive || dependency.isDevDependency) { |
| continue; |
| } |
| if (dependency.kind == DependencyKind.sdk) { |
| output.writeln(' ${dependency.name}:\n sdk: flutter'); |
| continue; |
| } |
| output.writeln(' ${dependency.name}: any'); |
| } |
| pubspecFile.writeAsStringSync(output.toString()); |
| } |
| |
| // Create the sky engine pubspec.yaml |
| directory |
| .childDirectory('bin') |
| .childDirectory('cache') |
| .childDirectory('pkg') |
| .childDirectory('sky_engine') |
| .childFile('pubspec.yaml') |
| ..createSync(recursive: true) |
| ..writeAsStringSync(''' |
| name: sky_engine |
| version: 0.0.99 |
| description: Dart SDK extensions for dart:ui |
| homepage: http://flutter.io |
| # sky_engine requires sdk_ext support in the analyzer which was added in 1.11.x |
| environment: |
| sdk: '>=1.11.0 <4.0.0' |
| '''); |
| |
| return directory; |
| } |