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
5import 'package:args/args.dart';
6import 'package:package_config/package_config.dart';
7import 'package:unified_analytics/unified_analytics.dart';
8
9import '../base/common.dart';
10import '../base/os.dart';
11import '../base/utils.dart';
12import '../build_info.dart';
13import '../build_system/build_system.dart';
14import '../build_system/targets/localizations.dart';
15import '../cache.dart';
16import '../dart/package_map.dart';
17import '../dart/pub.dart';
18import '../flutter_plugins.dart';
19import '../globals.dart' as globals;
20import '../package_graph.dart';
21import '../plugins.dart';
22import '../project.dart';
23import '../runner/flutter_command.dart';
24
25class 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
103class 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
133class 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
175class 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.
203class 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