flutter_command_runner.dart 13.6 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
import 'package:meta/meta.dart';
10

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

Ian Hickson's avatar
Ian Hickson committed
24 25 26
const String kFlutterRootEnvironmentVariableName = 'FLUTTER_ROOT'; // should point to //flutter/ (root of flutter/flutter repo)
const String kFlutterEngineEnvironmentVariableName = 'FLUTTER_ENGINE'; // should point to //engine/src/ (root of flutter/engine repo)
const String kSnapshotFileName = 'flutter_tools.snapshot'; // in //flutter/bin/cache/
27
const String kFlutterToolsScriptFileName = 'flutter_tools.dart'; // in //flutter/packages/flutter_tools/bin/
Ian Hickson's avatar
Ian Hickson committed
28 29
const String kFlutterEnginePackageName = 'sky_engine';

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

Ian Hickson's avatar
Ian Hickson committed
87
    String packagesHelp;
88
    bool showPackagesCommand;
89
    if (globals.fs.isFileSync(kPackagesFileName)) {
90 91 92 93 94 95
      packagesHelp = '(defaults to "$kPackagesFileName")';
      showPackagesCommand = verboseHelp;
    } else {
      packagesHelp = '(required, since the current directory does not contain a "$kPackagesFileName" file)';
      showPackagesCommand = true;
    }
96
    argParser.addOption('packages',
97 98 99
        hide: !showPackagesCommand,
        help: 'Path to your ".packages" file.\n$packagesHelp');

100
    argParser.addOption('flutter-root',
101 102
        hide: !verboseHelp,
        help: 'The root directory of the Flutter repository.\n'
103 104
              'Defaults to \$$kFlutterRootEnvironmentVariableName if set, otherwise uses the parent '
              'of the directory that the "flutter" script itself is in.');
Ian Hickson's avatar
Ian Hickson committed
105

106
    if (verboseHelp) {
Devon Carew's avatar
Devon Carew committed
107
      argParser.addSeparator('Local build selection options (not normally required):');
108
    }
109

110
    argParser.addOption('local-engine-src-path',
Devon Carew's avatar
Devon Carew committed
111
        hide: !verboseHelp,
112
        help: 'Path to your engine src directory, if you are building Flutter locally.\n'
113 114 115 116
              'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to '
              'the path given in your pubspec.yaml dependency_overrides for $kFlutterEnginePackageName, '
              'if any, or, failing that, tries to guess at the location based on the value of the '
              '--flutter-root option.');
117

118 119
    argParser.addOption('local-engine',
        hide: !verboseHelp,
120 121 122 123
        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'
              'This path is relative to --local-engine-src-path/out.');

124
    if (verboseHelp) {
125
      argParser.addSeparator('Options for testing the "flutter" tool itself:');
126
    }
127 128 129
    argParser.addFlag('show-test-device',
        negatable: false,
        hide: !verboseHelp,
130
        help: "List the special 'flutter-tester' device in device listings. "
131
              'This headless device is used to\ntest Flutter tooling.');
132 133
  }

134 135
  @override
  ArgParser get argParser => _argParser;
136 137
  final ArgParser _argParser = ArgParser(
    allowTrailingOptions: false,
138
    usageLineLength: globals.outputPreferences.wrapText ? globals.outputPreferences.wrapColumn : null,
139
  );
140

141
  @override
Hixie's avatar
Hixie committed
142
  String get usageFooter {
143
    return wrapText('Run "flutter help -v" for verbose help output, including less commonly used options.',
144 145
      columnWidth: globals.outputPreferences.wrapColumn,
      shouldWrap: globals.outputPreferences.wrapText,
146
    );
147 148 149 150 151
  }

  @override
  String get usage {
    final String usageWithoutDescription = super.usage.substring(description.length + 2);
152
    final String prefix = wrapText(description,
153 154
      shouldWrap: globals.outputPreferences.wrapText,
      columnWidth: globals.outputPreferences.wrapColumn,
155 156
    );
    return '$prefix\n\n$usageWithoutDescription';
Hixie's avatar
Hixie committed
157
  }
Devon Carew's avatar
Devon Carew committed
158

159 160 161 162 163 164 165
  @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.
166
      return tryArgsCompletion(args.toList(), argParser);
167 168 169 170 171
    } on ArgParserException catch (error) {
      if (error.commands.isEmpty) {
        usageException(error.message);
      }

172
      Command<void> command = commands[error.commands.first];
173
      for (final String commandName in error.commands.skip(1)) {
174 175 176 177 178 179 180 181
        command = command.subcommands[commandName];
      }

      command.usageException(error.message);
      return null;
    }
  }

182
  @override
183
  Future<void> run(Iterable<String> args) {
184
    // Have an invocation of 'build' print out it's sub-commands.
185
    // TODO(ianh): Move this to the Build command itself somehow.
186
    if (args.length == 1 && args.first == 'build') {
187
      args = <String>['build', '-h'];
188
    }
189

190
    return super.run(args);
Devon Carew's avatar
Devon Carew committed
191 192
  }

193
  @override
194
  Future<void> runCommand(ArgResults topLevelResults) async {
Jonah Williams's avatar
Jonah Williams committed
195
    final Map<Type, dynamic> contextOverrides = <Type, dynamic>{};
196

197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222
    // 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.
    int wrapColumn;
    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,
      showColor: topLevelResults['color'] as bool,
      wrapColumn: wrapColumn,
    );

223
    if (topLevelResults['show-test-device'] as bool ||
224 225 226 227
        topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) {
      FlutterTesterDevices.showFlutterTesterDevice = true;
    }

228 229
    // We must set Cache.flutterRoot early because other features use it (e.g.
    // enginePath's initializer uses it).
230 231 232 233 234
    final String flutterRoot = topLevelResults['flutter-root'] as String ?? Cache.defaultFlutterRoot(
      platform: globals.platform,
      fileSystem: globals.fs,
      userMessages: globals.userMessages,
    );
235
    Cache.flutterRoot = globals.fs.path.normalize(globals.fs.path.absolute(flutterRoot));
236

237
    // Set up the tooling configuration.
238 239 240 241 242
    final EngineBuildPaths engineBuildPaths = await globals.localEngineLocator.findEnginePath(
      topLevelResults['local-engine-src-path'] as String,
      topLevelResults['local-engine'] as String
    );
    if (engineBuildPaths != null) {
243
      contextOverrides.addAll(<Type, dynamic>{
244
        Artifacts: Artifacts.getLocalEngine(engineBuildPaths),
245
      });
246 247
    }

248
    await context.run<void>(
249
      overrides: contextOverrides.map<Type, Generator>((Type type, dynamic value) {
250
        return MapEntry<Type, Generator>(type, () => value);
251 252
      }),
      body: () async {
253
        globals.logger.quiet = topLevelResults['quiet'] as bool;
254

255
        if (globals.platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') {
256
          await Cache.lock();
257
        }
258

259
        if (topLevelResults['suppress-analytics'] as bool) {
260
          globals.flutterUsage.suppressAnalytics = true;
261
        }
262

263
        try {
264
          await globals.flutterVersion.ensureVersionFile();
265
        } on FileSystemException catch (e) {
266 267
          globals.printError('Failed to write the version file to the artifact cache: "$e".');
          globals.printError('Please ensure you have permissions in the artifact cache directory.');
268 269
          throwToolExit('Failed to write the version file');
        }
270 271
        final bool machineFlag = topLevelResults['machine'] as bool;
        if (topLevelResults.command?.name != 'upgrade' && topLevelResults['version-check'] as bool && !machineFlag) {
272
          await globals.flutterVersion.checkFlutterVersionFreshness();
273 274
        }

275
        if (topLevelResults.wasParsed('packages')) {
276
          globalPackagesPath = globals.fs.path.normalize(globals.fs.path.absolute(topLevelResults['packages'] as String));
277
        }
278 279

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

282
        if (topLevelResults['version'] as bool) {
283
          globals.flutterUsage.sendCommand('version');
284
          globals.flutterVersion.fetchTagsAndUpdate();
285
          String status;
286
          if (machineFlag) {
287 288 289 290 291
            final Map<String, Object> jsonOut = globals.flutterVersion.toJson();
            if (jsonOut != null) {
              jsonOut['flutterRoot'] = Cache.flutterRoot;
            }
            status = const JsonEncoder.withIndent('  ').convert(jsonOut);
292
          } else {
293
            status = globals.flutterVersion.toString();
294
          }
295
          globals.printStatus(status);
296 297 298
          return;
        }

299
        if (machineFlag) {
300 301 302 303 304
          throwToolExit('The --machine flag is only valid with the --version flag.', exitCode: 2);
        }
        await super.runCommand(topLevelResults);
      },
    );
Adam Barth's avatar
Adam Barth committed
305 306
  }

307
  @visibleForTesting
308
  static void initFlutterRoot() {
309 310 311 312 313
    Cache.flutterRoot ??= Cache.defaultFlutterRoot(
      platform: globals.platform,
      fileSystem: globals.fs,
      userMessages: globals.userMessages,
    );
314 315
  }

316 317
  /// Get the root directories of the repo - the directories containing Dart packages.
  List<String> getRepoRoots() {
318
    final String root = globals.fs.path.absolute(Cache.flutterRoot);
319
    // not bin, and not the root
320
    return <String>['dev', 'examples', 'packages'].map<String>((String item) {
321
      return globals.fs.path.join(root, item);
322 323 324 325 326 327 328
    }).toList();
  }

  /// Get all pub packages in the Flutter repo.
  List<Directory> getRepoPackages() {
    return getRepoRoots()
      .expand<String>((String root) => _gatherProjectPaths(root))
329
      .map<Directory>((String dir) => globals.fs.directory(dir))
330 331 332 333
      .toList();
  }

  static List<String> _gatherProjectPaths(String rootPath) {
334
    if (globals.fs.isFileSync(globals.fs.path.join(rootPath, '.dartignore'))) {
335
      return <String>[];
336
    }
337 338


339
    final List<String> projectPaths = globals.fs.directory(rootPath)
340 341
      .listSync(followLinks: false)
      .expand((FileSystemEntity entity) {
342
        if (entity is Directory && !globals.fs.path.split(entity.path).contains('.dart_tool')) {
343 344 345
          return _gatherProjectPaths(entity.path);
        }
        return <String>[];
346 347
      })
      .toList();
348

349
    if (globals.fs.isFileSync(globals.fs.path.join(rootPath, 'pubspec.yaml'))) {
350
      projectPaths.add(rootPath);
351
    }
352 353

    return projectPaths;
354
  }
355
}