// 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', // Keep pinned to latest until 1.0.0. 'material_color_utilities': '0.5.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/122039 'flutter_plugin_android_lifecycle': '2.0.8', }; 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[kFlutterStorageBaseUrl] ?? '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 = boolArg('force-upgrade'); final bool isPrintPaths = boolArg('paths'); final bool isPrintTransitiveClosure = boolArg('transitive-closure'); final bool isVerifyOnly = boolArg('verify-only'); final bool isConsumerOnly = boolArg('consumer-only'); final bool offline = boolArg('offline'); final bool doUpgrade = forceUpgrade || isPrintPaths || isPrintTransitiveClosure; if (boolArg('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: boolArg('offline'), flutterRootOverride: temporaryFlutterSdk?.path, outputMode: PubOutputMode.none, ); 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 (boolArg('transitive-closure')) { tree._dependencyTree.forEach((String from, Set<String> to) { globals.printStatus('$from -> $to'); }); return true; } if (boolArg('paths')) { showDependencyPaths(from: stringArg('from')!, to: stringArg('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(stringArg('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, outputMode: PubOutputMode.none, ); 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, isDevDependency: seenDev); if (dependency != null) { // We got one! 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) { 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; 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!); } } } 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, required this.isDevDependency, }) : _kind = kind; static PubspecDependency? parse( String line, { required String filename, required bool isDevDependency, }) { // 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, isDevDependency: isDevDependency, ); } 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 final 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 _lockIsOverride = false; 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(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); case DependencyKind.normal: if (!kManuallyPinnedDependencies.containsKey(name)) { dependencies.writeln(' $name: $versionToUse'); } case DependencyKind.path: if (_lockIsOverride) { dependencies.writeln(' $name: $versionToUse'); overrides.writeln(' $name:'); overrides.writeln(' path: $_lockTarget'); } else { dependencies.writeln(' $name:'); dependencies.writeln(' path: $_lockTarget'); } case DependencyKind.sdk: if (_lockIsOverride) { dependencies.writeln(' $name: $versionToUse'); overrides.writeln(' $name:'); overrides.writeln(' sdk: $_lockTarget'); } else { dependencies.writeln(' $name:'); dependencies.writeln(' sdk: $_lockTarget'); } case DependencyKind.git: if (_lockIsOverride) { dependencies.writeln(' $name: $versionToUse'); overrides.writeln(' $name:'); overrides.writeln(lockLine); } else { dependencies.writeln(' $name:'); dependencies.writeln(lockLine); } } } @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.12.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, }) { 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); 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: '>=3.0.0-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: '>=3.0.0-0 <4.0.0' '''); return directory; }