flutter_command_runner.dart 12.5 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4 5 6
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:args/args.dart';
import 'package:args/command_runner.dart';
7
import 'package:completion/completion.dart';
8
import 'package:file/file.dart';
9

10
import '../artifacts.dart';
11
import '../base/common.dart';
12
import '../base/context.dart';
13
import '../base/file_system.dart';
14
import '../base/terminal.dart';
15
import '../base/user_messages.dart';
16
import '../base/utils.dart';
17
import '../cache.dart';
18
import '../convert.dart';
19
import '../globals.dart' as globals;
20
import '../tester/flutter_tester.dart';
21
import '../web/web_device.dart';
22

23
class FlutterCommandRunner extends CommandRunner<void> {
24
  FlutterCommandRunner({ bool verboseHelp = false }) : super(
Devon Carew's avatar
Devon Carew committed
25
    'flutter',
26 27
    'Manage your Flutter app development.\n'
      '\n'
28
      'Common commands:\n'
29 30 31 32 33
      '\n'
      '  flutter create <output directory>\n'
      '    Create a new Flutter project in the specified directory.\n'
      '\n'
      '  flutter run [options]\n'
34
      '    Run your Flutter application on an attached device or in an emulator.',
Devon Carew's avatar
Devon Carew committed
35
  ) {
36 37 38
    argParser.addFlag('verbose',
        abbr: 'v',
        negatable: false,
39
        help: 'Noisy logging, including all shell commands executed.\n'
40 41 42
              'If used with "--help", shows hidden options. '
              'If used with "flutter doctor", shows additional diagnostic information. '
              '(Use "-vv" to force verbose logging in those cases.)');
43 44
    argParser.addFlag('prefixed-errors',
        negatable: false,
45
        help: 'Causes lines sent to stderr to be prefixed with "ERROR:".',
46
        hide: !verboseHelp);
47 48 49 50
    argParser.addFlag('quiet',
        negatable: false,
        hide: !verboseHelp,
        help: 'Reduce the amount of output from some commands.');
51 52 53 54 55 56
    argParser.addFlag('wrap',
        hide: !verboseHelp,
        help: 'Toggles output word wrapping, regardless of whether or not the output is a terminal.',
        defaultsTo: true);
    argParser.addOption('wrap-column',
        hide: !verboseHelp,
57
        help: 'Sets the output wrap column. If not set, uses the width of the terminal. No '
58
              'wrapping occurs if not writing to a terminal. Use "--no-wrap" to turn off wrapping '
59
              'when connected to a terminal.');
Devon Carew's avatar
Devon Carew committed
60 61
    argParser.addOption('device-id',
        abbr: 'd',
62
        help: 'Target device id or name (prefixes allowed).');
63 64 65
    argParser.addFlag('version',
        negatable: false,
        help: 'Reports the version of this tool.');
66
    argParser.addFlag('machine',
67
        negatable: false,
68
        hide: !verboseHelp,
69
        help: 'When used with the "--version" flag, outputs the information using JSON.');
70 71
    argParser.addFlag('color',
        hide: !verboseHelp,
72 73
        help: 'Whether to use terminal colors (requires support for ANSI escape sequences).',
        defaultsTo: true);
74 75 76 77
    argParser.addFlag('version-check',
        defaultsTo: true,
        hide: !verboseHelp,
        help: 'Allow Flutter to check for updates when this command runs.');
78 79 80
    argParser.addFlag('suppress-analytics',
        negatable: false,
        help: 'Suppress analytics reporting when this command runs.');
81
    argParser.addOption('packages',
82 83
        hide: !verboseHelp,
        help: 'Path to your "package_config.json" file.');
84
    if (verboseHelp) {
Devon Carew's avatar
Devon Carew committed
85
      argParser.addSeparator('Local build selection options (not normally required):');
86
    }
87

88
    argParser.addOption('local-engine-src-path',
Devon Carew's avatar
Devon Carew committed
89
        hide: !verboseHelp,
90
        help: 'Path to your engine src directory, if you are building Flutter locally.\n'
91 92
              'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to '
              'the path given in your pubspec.yaml dependency_overrides for $kFlutterEnginePackageName, '
93
              'if any.');
94

95 96
    argParser.addOption('local-engine',
        hide: !verboseHelp,
97 98
        help: 'Name of a build output within the engine out directory, if you are building Flutter locally.\n'
              'Use this to select a specific version of the engine if you have built multiple engine targets.\n'
99
              'This path is relative to "--local-engine-src-path" or "--local-engine-src-out" (q.v.).');
100

101
    if (verboseHelp) {
102
      argParser.addSeparator('Options for testing the "flutter" tool itself:');
103
    }
104 105 106
    argParser.addFlag('show-test-device',
        negatable: false,
        hide: !verboseHelp,
107 108
        help: 'List the special "flutter-tester" device in device listings. '
              'This headless device is used to test Flutter tooling.');
109 110 111
    argParser.addFlag('show-web-server-device',
        negatable: false,
        hide: !verboseHelp,
112
        help: 'List the special "web-server" device in device listings.',
113
    );
114 115
  }

116 117
  @override
  ArgParser get argParser => _argParser;
118 119
  final ArgParser _argParser = ArgParser(
    allowTrailingOptions: false,
120
    usageLineLength: globals.outputPreferences.wrapText ? globals.outputPreferences.wrapColumn : null,
121
  );
122

123
  @override
Hixie's avatar
Hixie committed
124
  String get usageFooter {
125
    return wrapText('Run "flutter help -v" for verbose help output, including less commonly used options.',
126 127
      columnWidth: globals.outputPreferences.wrapColumn,
      shouldWrap: globals.outputPreferences.wrapText,
128
    );
129 130 131 132 133
  }

  @override
  String get usage {
    final String usageWithoutDescription = super.usage.substring(description.length + 2);
134
    final String prefix = wrapText(description,
135 136
      shouldWrap: globals.outputPreferences.wrapText,
      columnWidth: globals.outputPreferences.wrapColumn,
137 138
    );
    return '$prefix\n\n$usageWithoutDescription';
Hixie's avatar
Hixie committed
139
  }
Devon Carew's avatar
Devon Carew committed
140

141 142 143 144 145 146 147
  @override
  ArgResults parse(Iterable<String> args) {
    try {
      // This is where the CommandRunner would call argParser.parse(args). We
      // override this function so we can call tryArgsCompletion instead, so the
      // completion package can interrogate the argParser, and as part of that,
      // it calls argParser.parse(args) itself and returns the result.
148
      return tryArgsCompletion(args.toList(), argParser);
149 150 151 152 153
    } on ArgParserException catch (error) {
      if (error.commands.isEmpty) {
        usageException(error.message);
      }

154
      Command<void>? command = commands[error.commands.first];
155
      for (final String commandName in error.commands.skip(1)) {
156
        command = command?.subcommands[commandName];
157 158
      }

159
      command!.usageException(error.message);
160 161 162
    }
  }

163
  @override
164
  Future<void> run(Iterable<String> args) {
165
    // Have an invocation of 'build' print out it's sub-commands.
166
    // TODO(ianh): Move this to the Build command itself somehow.
167 168 169 170 171 172
    if (args.length == 1) {
      if (args.first == 'build') {
        args = <String>['build', '-h'];
      } else if (args.first == 'custom-devices') {
        args = <String>['custom-devices', '-h'];
      }
173
    }
174

175
    return super.run(args);
Devon Carew's avatar
Devon Carew committed
176 177
  }

178
  @override
179
  Future<void> runCommand(ArgResults topLevelResults) async {
180
    final Map<Type, Object?> contextOverrides = <Type, Object?>{};
181

182 183 184
    // Don't set wrapColumns unless the user said to: if it's set, then all
    // wrapping will occur at this width explicitly, and won't adapt if the
    // terminal size changes during a run.
185
    int? wrapColumn;
186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
    if (topLevelResults.wasParsed('wrap-column')) {
      try {
        wrapColumn = int.parse(topLevelResults['wrap-column'] as String);
        if (wrapColumn < 0) {
          throwToolExit(userMessages.runnerWrapColumnInvalid(topLevelResults['wrap-column']));
        }
      } on FormatException {
        throwToolExit(userMessages.runnerWrapColumnParseError(topLevelResults['wrap-column']));
      }
    }

    // If we're not writing to a terminal with a defined width, then don't wrap
    // anything, unless the user explicitly said to.
    final bool useWrapping = topLevelResults.wasParsed('wrap')
        ? topLevelResults['wrap'] as bool
        : globals.stdio.terminalColumns != null && topLevelResults['wrap'] as bool;
    contextOverrides[OutputPreferences] = OutputPreferences(
      wrapText: useWrapping,
204
      showColor: topLevelResults['color'] as bool?,
205 206 207
      wrapColumn: wrapColumn,
    );

208
    if ((topLevelResults['show-test-device'] as bool?) == true ||
209 210 211
        topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) {
      FlutterTesterDevices.showFlutterTesterDevice = true;
    }
212
    if ((topLevelResults['show-web-server-device'] as bool?) == true  ||
213 214 215
        topLevelResults['device-id'] == WebServerDevice.kWebServerDeviceId) {
      WebServerDevice.showWebServerDevice = true;
    }
216

217
    // Set up the tooling configuration.
218 219 220 221
    final EngineBuildPaths? engineBuildPaths = await globals.localEngineLocator?.findEnginePath(
      topLevelResults['local-engine-src-path'] as String?,
      topLevelResults['local-engine'] as String?,
      topLevelResults['packages'] as String?,
222 223
    );
    if (engineBuildPaths != null) {
224
      contextOverrides.addAll(<Type, Object?>{
225
        Artifacts: Artifacts.getLocalEngine(engineBuildPaths),
226
      });
227 228
    }

229
    await context.run<void>(
230
      overrides: contextOverrides.map<Type, Generator>((Type type, Object? value) {
231
        return MapEntry<Type, Generator>(type, () => value);
232 233
      }),
      body: () async {
234
        globals.logger.quiet = (topLevelResults['quiet'] as bool?) == true;
235

236
        if (globals.platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') {
237
          await globals.cache.lock();
238
        }
239

240
        if ((topLevelResults['suppress-analytics'] as bool?) == true) {
241
          globals.flutterUsage.suppressAnalytics = true;
242
        }
243

244
        globals.flutterVersion.ensureVersionFile();
245
        final bool machineFlag = topLevelResults['machine'] as bool? ?? false;
246
        final bool ci = await globals.botDetector.isRunningOnBot;
247 248 249
        final bool redirectedCompletion = !globals.stdio.hasTerminal &&
            (topLevelResults.command?.name ?? '').endsWith('-completion');
        final bool isMachine = machineFlag || ci || redirectedCompletion;
250
        final bool versionCheckFlag = topLevelResults['version-check'] as bool? ?? false;
251 252 253
        final bool explicitVersionCheckPassed = topLevelResults.wasParsed('version-check') && versionCheckFlag;

        if (topLevelResults.command?.name != 'upgrade' &&
254
            (explicitVersionCheckPassed || (versionCheckFlag && !isMachine))) {
255
          await globals.flutterVersion.checkFlutterVersionFreshness();
256 257 258
        }

        // See if the user specified a specific device.
259
        globals.deviceManager?.specifiedDeviceId = topLevelResults['device-id'] as String?;
260

261
        if ((topLevelResults['version'] as bool?) == true) {
262
          globals.flutterUsage.sendCommand('version');
263
          globals.flutterVersion.fetchTagsAndUpdate();
264
          String status;
265
          if (machineFlag) {
266 267
            final Map<String, Object> jsonOut = globals.flutterVersion.toJson();
            if (jsonOut != null) {
268
              jsonOut['flutterRoot'] = Cache.flutterRoot!;
269 270
            }
            status = const JsonEncoder.withIndent('  ').convert(jsonOut);
271
          } else {
272
            status = globals.flutterVersion.toString();
273
          }
274
          globals.printStatus(status);
275 276 277
          return;
        }

278
        if (machineFlag) {
279
          throwToolExit('The "--machine" flag is only valid with the "--version" flag.', exitCode: 2);
280 281 282 283
        }
        await super.runCommand(topLevelResults);
      },
    );
Adam Barth's avatar
Adam Barth committed
284 285
  }

286 287
  /// Get the root directories of the repo - the directories containing Dart packages.
  List<String> getRepoRoots() {
288
    final String root = globals.fs.path.absolute(Cache.flutterRoot!);
289
    // not bin, and not the root
290
    return <String>['dev', 'examples', 'packages'].map<String>((String item) {
291
      return globals.fs.path.join(root, item);
292 293 294 295 296 297 298
    }).toList();
  }

  /// Get all pub packages in the Flutter repo.
  List<Directory> getRepoPackages() {
    return getRepoRoots()
      .expand<String>((String root) => _gatherProjectPaths(root))
299
      .map<Directory>((String dir) => globals.fs.directory(dir))
300 301 302 303
      .toList();
  }

  static List<String> _gatherProjectPaths(String rootPath) {
304
    if (globals.fs.isFileSync(globals.fs.path.join(rootPath, '.dartignore'))) {
305
      return <String>[];
306
    }
307

308
    final List<String> projectPaths = globals.fs.directory(rootPath)
309 310
      .listSync(followLinks: false)
      .expand((FileSystemEntity entity) {
311
        if (entity is Directory && !globals.fs.path.split(entity.path).contains('.dart_tool')) {
312 313 314
          return _gatherProjectPaths(entity.path);
        }
        return <String>[];
315 316
      })
      .toList();
317

318
    if (globals.fs.isFileSync(globals.fs.path.join(rootPath, 'pubspec.yaml'))) {
319
      projectPaths.add(rootPath);
320
    }
321 322

    return projectPaths;
323
  }
324
}