| 1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
| 2 | // Use of this source code is governed by a BSD-style license that can be |
| 3 | // found in the LICENSE file. |
| 4 | |
| 5 | import 'package:args/args.dart' ; |
| 6 | import 'package:package_config/package_config.dart' ; |
| 7 | import 'package:unified_analytics/unified_analytics.dart' ; |
| 8 | |
| 9 | import '../base/common.dart'; |
| 10 | import '../base/os.dart'; |
| 11 | import '../base/utils.dart'; |
| 12 | import '../build_info.dart'; |
| 13 | import '../build_system/build_system.dart'; |
| 14 | import '../build_system/targets/localizations.dart'; |
| 15 | import '../cache.dart'; |
| 16 | import '../dart/package_map.dart'; |
| 17 | import '../dart/pub.dart'; |
| 18 | import '../flutter_plugins.dart'; |
| 19 | import '../globals.dart' as globals; |
| 20 | import '../package_graph.dart'; |
| 21 | import '../plugins.dart'; |
| 22 | import '../project.dart'; |
| 23 | import '../runner/flutter_command.dart'; |
| 24 | |
| 25 | class PackagesCommand extends FlutterCommand { |
| 26 | PackagesCommand() { |
| 27 | addSubcommand( |
| 28 | PackagesGetCommand('get' , "Get the current package's dependencies." , PubContext.pubGet), |
| 29 | ); |
| 30 | addSubcommand( |
| 31 | PackagesGetCommand( |
| 32 | 'upgrade' , |
| 33 | "Upgrade the current package's dependencies to latest versions." , |
| 34 | PubContext.pubUpgrade, |
| 35 | ), |
| 36 | ); |
| 37 | addSubcommand( |
| 38 | PackagesGetCommand('add' , 'Add a dependency to pubspec.yaml.' , PubContext.pubAdd), |
| 39 | ); |
| 40 | addSubcommand( |
| 41 | PackagesGetCommand( |
| 42 | 'remove' , |
| 43 | 'Removes a dependency from the current package.' , |
| 44 | PubContext.pubRemove, |
| 45 | ), |
| 46 | ); |
| 47 | addSubcommand(PackagesTestCommand()); |
| 48 | addSubcommand( |
| 49 | PackagesForwardCommand( |
| 50 | 'publish' , |
| 51 | 'Publish the current package to pub.dartlang.org.' , |
| 52 | requiresPubspec: true, |
| 53 | ), |
| 54 | ); |
| 55 | addSubcommand( |
| 56 | PackagesForwardCommand( |
| 57 | 'downgrade' , |
| 58 | 'Downgrade packages in a Flutter project.' , |
| 59 | requiresPubspec: true, |
| 60 | ), |
| 61 | ); |
| 62 | addSubcommand( |
| 63 | PackagesForwardCommand('deps' , 'Print package dependencies.' ), |
| 64 | ); // path to package can be specified with --directory argument |
| 65 | addSubcommand( |
| 66 | PackagesForwardCommand('run' , 'Run an executable from a package.' , requiresPubspec: true), |
| 67 | ); |
| 68 | addSubcommand(PackagesForwardCommand('cache' , 'Work with the Pub system cache.' )); |
| 69 | addSubcommand(PackagesForwardCommand('version' , 'Print Pub version.' )); |
| 70 | addSubcommand(PackagesForwardCommand('uploader' , 'Manage uploaders for a package on pub.dev.' )); |
| 71 | addSubcommand(PackagesForwardCommand('login' , 'Log into pub.dev.' )); |
| 72 | addSubcommand(PackagesForwardCommand('logout' , 'Log out of pub.dev.' )); |
| 73 | addSubcommand(PackagesForwardCommand('global' , 'Work with Pub global packages.' )); |
| 74 | addSubcommand( |
| 75 | PackagesForwardCommand( |
| 76 | 'outdated' , |
| 77 | 'Analyze dependencies to find which ones can be upgraded.' , |
| 78 | requiresPubspec: true, |
| 79 | ), |
| 80 | ); |
| 81 | addSubcommand( |
| 82 | PackagesForwardCommand('token' , 'Manage authentication tokens for hosted pub repositories.' ), |
| 83 | ); |
| 84 | addSubcommand(PackagesPassthroughCommand()); |
| 85 | } |
| 86 | |
| 87 | @override |
| 88 | final name = 'pub' ; |
| 89 | |
| 90 | @override |
| 91 | List<String> get aliases => const <String>['packages' ]; |
| 92 | |
| 93 | @override |
| 94 | final description = 'Commands for managing Flutter packages.' ; |
| 95 | |
| 96 | @override |
| 97 | String get category => FlutterCommandCategory.project; |
| 98 | |
| 99 | @override |
| 100 | Future<FlutterCommandResult> runCommand() async => FlutterCommandResult.fail(); |
| 101 | } |
| 102 | |
| 103 | class PackagesTestCommand extends FlutterCommand { |
| 104 | PackagesTestCommand() { |
| 105 | requiresPubspecYaml(); |
| 106 | } |
| 107 | |
| 108 | @override |
| 109 | String get name => 'test' ; |
| 110 | |
| 111 | @override |
| 112 | String get description { |
| 113 | return 'Run the "test" package.\n' |
| 114 | 'This is similar to "flutter test", but instead of hosting the tests in the ' |
| 115 | 'flutter environment it hosts the tests in a pure Dart environment. The main ' |
| 116 | 'differences are that the "dart:ui" library is not available and that tests ' |
| 117 | 'run faster. This is helpful for testing libraries that do not depend on any ' |
| 118 | 'packages from the Flutter SDK. It is equivalent to "pub run test".' ; |
| 119 | } |
| 120 | |
| 121 | @override |
| 122 | String get invocation { |
| 123 | return ' ${runner!.executableName} pub test [<tests...>]' ; |
| 124 | } |
| 125 | |
| 126 | @override |
| 127 | Future<FlutterCommandResult> runCommand() async { |
| 128 | await pub.batch(<String>['run' , 'test' , ...argResults!.rest], context: PubContext.runTest); |
| 129 | return FlutterCommandResult.success(); |
| 130 | } |
| 131 | } |
| 132 | |
| 133 | class PackagesForwardCommand extends FlutterCommand { |
| 134 | PackagesForwardCommand(this._commandName, this._description, {bool requiresPubspec = false}) { |
| 135 | if (requiresPubspec) { |
| 136 | requiresPubspecYaml(); |
| 137 | } |
| 138 | } |
| 139 | |
| 140 | PubContext context = PubContext.pubForward; |
| 141 | |
| 142 | @override |
| 143 | var argParser = ArgParser.allowAnything(); |
| 144 | |
| 145 | final String _commandName; |
| 146 | final String _description; |
| 147 | |
| 148 | @override |
| 149 | String get name => _commandName; |
| 150 | |
| 151 | @override |
| 152 | String get description { |
| 153 | return ' $_description\n' |
| 154 | 'This runs the "pub" tool in a Flutter context.' ; |
| 155 | } |
| 156 | |
| 157 | @override |
| 158 | String get invocation { |
| 159 | return ' ${runner!.executableName} pub $_commandName [<arguments...>]' ; |
| 160 | } |
| 161 | |
| 162 | @override |
| 163 | Future<FlutterCommandResult> runCommand() async { |
| 164 | final List<String> subArgs = argResults!.rest.toList() |
| 165 | ..removeWhere((String arg) => arg == '--' ); |
| 166 | await pub.interactively( |
| 167 | <String>[_commandName, ...subArgs], |
| 168 | context: context, |
| 169 | command: _commandName, |
| 170 | ); |
| 171 | return FlutterCommandResult.success(); |
| 172 | } |
| 173 | } |
| 174 | |
| 175 | class PackagesPassthroughCommand extends FlutterCommand { |
| 176 | @override |
| 177 | var argParser = ArgParser.allowAnything(); |
| 178 | |
| 179 | @override |
| 180 | String get name => 'pub' ; |
| 181 | |
| 182 | @override |
| 183 | String get description { |
| 184 | return 'Pass the remaining arguments to Dart\'s "pub" tool.\n' |
| 185 | 'This runs the "pub" tool in a Flutter context.' ; |
| 186 | } |
| 187 | |
| 188 | @override |
| 189 | String get invocation { |
| 190 | return ' ${runner!.executableName} packages pub [<arguments...>]' ; |
| 191 | } |
| 192 | |
| 193 | static final PubContext _context = PubContext.pubPassThrough; |
| 194 | |
| 195 | @override |
| 196 | Future<FlutterCommandResult> runCommand() async { |
| 197 | await pub.interactively(command: 'pub' , argResults!.rest, context: _context); |
| 198 | return FlutterCommandResult.success(); |
| 199 | } |
| 200 | } |
| 201 | |
| 202 | /// Represents the pub sub-commands that makes package-resolutions. |
| 203 | class PackagesGetCommand extends FlutterCommand { |
| 204 | PackagesGetCommand(this._commandName, this._description, this._context); |
| 205 | |
| 206 | @override |
| 207 | var argParser = ArgParser.allowAnything(); |
| 208 | |
| 209 | final String _commandName; |
| 210 | final String _description; |
| 211 | final PubContext _context; |
| 212 | |
| 213 | FlutterProject? _rootProject; |
| 214 | |
| 215 | @override |
| 216 | String get name => _commandName; |
| 217 | |
| 218 | @override |
| 219 | String get description { |
| 220 | return ' $_description\n' |
| 221 | 'This runs the "pub" tool in a Flutter context.' ; |
| 222 | } |
| 223 | |
| 224 | @override |
| 225 | String get invocation { |
| 226 | return ' ${runner!.executableName} pub $_commandName [<arguments...>]' ; |
| 227 | } |
| 228 | |
| 229 | /// An [ArgParser] that accepts all options and flags that the |
| 230 | /// |
| 231 | /// `pub get` |
| 232 | /// `pub upgrade` |
| 233 | /// `pub downgrade` |
| 234 | /// `pub add` |
| 235 | /// `pub remove` |
| 236 | /// |
| 237 | /// commands accept. |
| 238 | ArgParser get _permissiveArgParser { |
| 239 | final argParser = ArgParser(); |
| 240 | argParser.addOption('directory' , abbr: 'C' ); |
| 241 | argParser.addFlag('offline' ); |
| 242 | argParser.addFlag('dry-run' , abbr: 'n' ); |
| 243 | argParser.addFlag('help' , abbr: 'h' ); |
| 244 | argParser.addFlag('enforce-lockfile' ); |
| 245 | argParser.addFlag('precompile' ); |
| 246 | argParser.addFlag('major-versions' ); |
| 247 | argParser.addFlag('example' , defaultsTo: true); |
| 248 | argParser.addOption('sdk' ); |
| 249 | argParser.addOption('path' ); |
| 250 | argParser.addOption('hosted-url' ); |
| 251 | argParser.addOption('git-url' ); |
| 252 | argParser.addOption('git-ref' ); |
| 253 | argParser.addOption('git-path' ); |
| 254 | argParser.addFlag('dev' ); |
| 255 | argParser.addFlag('verbose' , abbr: 'v' ); |
| 256 | return argParser; |
| 257 | } |
| 258 | |
| 259 | @override |
| 260 | Future<FlutterCommandResult> runCommand() async { |
| 261 | final List<String> rest = argResults!.rest; |
| 262 | var isHelp = false; |
| 263 | var example = true; |
| 264 | var exampleWasParsed = false; |
| 265 | String? directoryOption; |
| 266 | var dryRun = false; |
| 267 | try { |
| 268 | final ArgResults results = _permissiveArgParser.parse(rest); |
| 269 | isHelp = results['help' ] as bool; |
| 270 | directoryOption = results['directory' ] as String?; |
| 271 | example = results['example' ] as bool; |
| 272 | exampleWasParsed = results.wasParsed('example' ); |
| 273 | dryRun = results['dry-run' ] as bool; |
| 274 | } on ArgParserException { |
| 275 | // Let pub give the error message. |
| 276 | } |
| 277 | String? target; |
| 278 | FlutterProject? rootProject; |
| 279 | |
| 280 | if (!isHelp) { |
| 281 | target = findProjectRoot(globals.fs, directoryOption); |
| 282 | if (target == null) { |
| 283 | if (directoryOption == null) { |
| 284 | throwToolExit('Expected to find project root in current working directory.' ); |
| 285 | } else { |
| 286 | throwToolExit('Expected to find project root in $directoryOption.' ); |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | rootProject = FlutterProject.fromDirectory(globals.fs.directory(target)); |
| 291 | _rootProject = rootProject; |
| 292 | } |
| 293 | final String? relativeTarget = target == null ? null : globals.fs.path.relative(target); |
| 294 | |
| 295 | final List<String> subArgs = rest.toList()..removeWhere((String arg) => arg == '--' ); |
| 296 | final timer = Stopwatch()..start(); |
| 297 | try { |
| 298 | await pub.interactively( |
| 299 | <String>[ |
| 300 | name, |
| 301 | ...subArgs, |
| 302 | // `dart pub get` and friends defaults to `--no-example`. |
| 303 | if (!exampleWasParsed && target != null) '--example' , |
| 304 | if (directoryOption == null && relativeTarget != null) ...<String>[ |
| 305 | '--directory' , |
| 306 | relativeTarget, |
| 307 | ], |
| 308 | ], |
| 309 | project: rootProject, |
| 310 | context: _context, |
| 311 | command: name, |
| 312 | touchesPackageConfig: !(isHelp || dryRun), |
| 313 | ); |
| 314 | final Duration elapsedDuration = timer.elapsed; |
| 315 | analytics.send( |
| 316 | Event.timing( |
| 317 | workflow: 'pub' , |
| 318 | variableName: 'get' , |
| 319 | elapsedMilliseconds: elapsedDuration.inMilliseconds, |
| 320 | label: 'success' , |
| 321 | ), |
| 322 | ); |
| 323 | // Not limiting to catching Exception because the exception is rethrown. |
| 324 | } catch (_) { |
| 325 | final Duration elapsedDuration = timer.elapsed; |
| 326 | analytics.send( |
| 327 | Event.timing( |
| 328 | workflow: 'pub' , |
| 329 | variableName: 'get' , |
| 330 | elapsedMilliseconds: elapsedDuration.inMilliseconds, |
| 331 | label: 'failure' , |
| 332 | ), |
| 333 | ); |
| 334 | rethrow; |
| 335 | } |
| 336 | |
| 337 | if (rootProject != null) { |
| 338 | // Walk through all workspace projects,and generate platform specific |
| 339 | // tooling if needed. |
| 340 | final PackageConfig packageConfig = await loadPackageConfigWithLogging( |
| 341 | rootProject.packageConfig, |
| 342 | logger: globals.logger, |
| 343 | ); |
| 344 | final PackageGraph graph = PackageGraph.load(rootProject); |
| 345 | // Iterate all root packages in the pub workspace to do Flutter specific |
| 346 | // generation. |
| 347 | for (final String workspaceRootName in graph.roots) { |
| 348 | final Package? rootPackage = packageConfig[workspaceRootName]; |
| 349 | assert(rootPackage != null); |
| 350 | final Uri rootUri = rootPackage!.root; |
| 351 | |
| 352 | final FlutterProject project = FlutterProject.fromDirectory(globals.fs.directory(rootUri)); |
| 353 | |
| 354 | final environment = Environment( |
| 355 | artifacts: globals.artifacts!, |
| 356 | logger: globals.logger, |
| 357 | cacheDir: globals.cache.getRoot(), |
| 358 | engineVersion: globals.flutterVersion.engineRevision, |
| 359 | fileSystem: globals.fs, |
| 360 | flutterRootDir: globals.fs.directory(Cache.flutterRoot), |
| 361 | outputDir: globals.fs.directory(getBuildDirectory()), |
| 362 | processManager: globals.processManager, |
| 363 | platform: globals.platform, |
| 364 | analytics: analytics, |
| 365 | projectDir: project.directory, |
| 366 | packageConfigPath: packageConfigPath(), |
| 367 | generateDartPluginRegistry: true, |
| 368 | ); |
| 369 | if (project.manifest.generateLocalizations) { |
| 370 | // If localizations were enabled, but we are not using synthetic packages. |
| 371 | final BuildResult result = await globals.buildSystem.build( |
| 372 | const GenerateLocalizationsTarget(), |
| 373 | environment, |
| 374 | ); |
| 375 | if (result.hasException) { |
| 376 | throwToolExit( |
| 377 | 'Generating synthetic localizations package failed with ${result.exceptions.length} ${pluralize('error' , result.exceptions.length)}:' |
| 378 | '\n\n' |
| 379 | ' ${result.exceptions.values.map<Object?>((ExceptionMeasurement e) => e.exception).join('\n\n' )}' , |
| 380 | ); |
| 381 | } |
| 382 | } |
| 383 | |
| 384 | // TODO(matanlurey): https://github.com/flutter/flutter/issues/163774. |
| 385 | // |
| 386 | // `flutter packages get` inherently is neither a debug or release build, |
| 387 | // and since a future build (`flutter build apk`) will regenerate tooling |
| 388 | // anyway, we assume this is fine. |
| 389 | // |
| 390 | // It won't be if they do `flutter build --no-pub`, though. |
| 391 | const ignoreReleaseModeSinceItsNotABuildAndHopeItWorks = false; |
| 392 | |
| 393 | // We need to regenerate the platform specific tooling for both the project |
| 394 | // itself and example(if present). |
| 395 | await project.regeneratePlatformSpecificTooling( |
| 396 | releaseMode: ignoreReleaseModeSinceItsNotABuildAndHopeItWorks, |
| 397 | ); |
| 398 | if (example && project.hasExampleApp && project.example.pubspecFile.existsSync()) { |
| 399 | final FlutterProject exampleProject = project.example; |
| 400 | await exampleProject.regeneratePlatformSpecificTooling( |
| 401 | releaseMode: ignoreReleaseModeSinceItsNotABuildAndHopeItWorks, |
| 402 | ); |
| 403 | } |
| 404 | } |
| 405 | } |
| 406 | |
| 407 | return FlutterCommandResult.success(); |
| 408 | } |
| 409 | |
| 410 | late final Future<List<Plugin>> _pluginsFound = (() async { |
| 411 | final FlutterProject? rootProject = _rootProject; |
| 412 | if (rootProject == null) { |
| 413 | return <Plugin>[]; |
| 414 | } |
| 415 | |
| 416 | return findPlugins(rootProject, throwOnError: false); |
| 417 | })(); |
| 418 | |
| 419 | late final String? _androidEmbeddingVersion = _rootProject?.android |
| 420 | .getEmbeddingVersion() |
| 421 | .toString() |
| 422 | .split('.' ) |
| 423 | .last; |
| 424 | |
| 425 | /// The pub packages usage values are incorrect since these are calculated/sent |
| 426 | /// before pub get completes. This needs to be performed after dependency resolution. |
| 427 | @override |
| 428 | Future<Event> unifiedAnalyticsUsageValues(String commandPath) async { |
| 429 | final FlutterProject? rootProject = _rootProject; |
| 430 | if (rootProject == null) { |
| 431 | return Event.commandUsageValues(workflow: commandPath, commandHasTerminal: hasTerminal); |
| 432 | } |
| 433 | |
| 434 | final int numberPlugins; |
| 435 | // Do not send plugin analytics if pub has not run before. |
| 436 | final bool hasPlugins = |
| 437 | rootProject.flutterPluginsDependenciesFile.existsSync() && |
| 438 | findPackageConfigFile(rootProject.directory) != null; |
| 439 | if (hasPlugins) { |
| 440 | // Do not fail pub get if package config files are invalid before pub has |
| 441 | // had a chance to run. |
| 442 | final List<Plugin> plugins = await _pluginsFound; |
| 443 | numberPlugins = plugins.length; |
| 444 | } else { |
| 445 | numberPlugins = 0; |
| 446 | } |
| 447 | |
| 448 | return Event.commandUsageValues( |
| 449 | workflow: commandPath, |
| 450 | commandHasTerminal: hasTerminal, |
| 451 | packagesNumberPlugins: numberPlugins, |
| 452 | packagesProjectModule: rootProject.isModule, |
| 453 | packagesAndroidEmbeddingVersion: _androidEmbeddingVersion, |
| 454 | ); |
| 455 | } |
| 456 | } |
| 457 | |