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

import 'dart:async';

import 'package:args/args.dart';
import 'package:args/command_runner.dart';
9
import 'package:completion/completion.dart';
10
import 'package:file/file.dart';
11
import 'package:meta/meta.dart';
12
import 'package:package_config/package_config.dart';
13

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

Ian Hickson's avatar
Ian Hickson committed
27 28 29
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/
30
const String kFlutterToolsScriptFileName = 'flutter_tools.dart'; // in //flutter/packages/flutter_tools/bin/
Ian Hickson's avatar
Ian Hickson committed
31 32
const String kFlutterEnginePackageName = 'sky_engine';

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

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

103
    argParser.addOption('flutter-root',
104 105
        hide: !verboseHelp,
        help: 'The root directory of the Flutter repository.\n'
106 107
              '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
108

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

113
    argParser.addOption('local-engine-src-path',
Devon Carew's avatar
Devon Carew committed
114
        hide: !verboseHelp,
115
        help: 'Path to your engine src directory, if you are building Flutter locally.\n'
116 117 118 119
              '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.');
120

121 122
    argParser.addOption('local-engine',
        hide: !verboseHelp,
123 124 125 126
        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.');

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

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

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

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

162
  static String get defaultFlutterRoot {
163 164
    if (globals.platform.environment.containsKey(kFlutterRootEnvironmentVariableName)) {
      return globals.platform.environment[kFlutterRootEnvironmentVariableName];
165
    }
Devon Carew's avatar
Devon Carew committed
166
    try {
167
      if (globals.platform.script.scheme == 'data') {
Hixie's avatar
Hixie committed
168
        return '../..'; // we're running as a test
169
      }
170

171 172 173
      if (globals.platform.script.scheme == 'package') {
        final String packageConfigPath = Uri.parse(globals.platform.packageConfig).toFilePath();
        return globals.fs.path.dirname(globals.fs.path.dirname(globals.fs.path.dirname(packageConfigPath)));
174 175
      }

176 177 178
      final String script = globals.platform.script.toFilePath();
      if (globals.fs.path.basename(script) == kSnapshotFileName) {
        return globals.fs.path.dirname(globals.fs.path.dirname(globals.fs.path.dirname(script)));
179
      }
180 181
      if (globals.fs.path.basename(script) == kFlutterToolsScriptFileName) {
        return globals.fs.path.dirname(globals.fs.path.dirname(globals.fs.path.dirname(globals.fs.path.dirname(script))));
182
      }
183 184

      // If run from a bare script within the repo.
185
      if (script.contains('flutter/packages/')) {
186
        return script.substring(0, script.indexOf('flutter/packages/') + 8);
187 188
      }
      if (script.contains('flutter/examples/')) {
189
        return script.substring(0, script.indexOf('flutter/examples/') + 8);
190
      }
191
    } on Exception catch (error) {
Hixie's avatar
Hixie committed
192 193
      // we don't have a logger at the time this is run
      // (which is why we don't use printTrace here)
194
      print(userMessages.runnerNoRoot('$error'));
Devon Carew's avatar
Devon Carew committed
195
    }
Ian Hickson's avatar
Ian Hickson committed
196 197 198
    return '.';
  }

199 200 201 202 203 204 205
  @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.
206
      return tryArgsCompletion(args.toList(), argParser);
207 208 209 210 211
    } on ArgParserException catch (error) {
      if (error.commands.isEmpty) {
        usageException(error.message);
      }

212
      Command<void> command = commands[error.commands.first];
213
      for (final String commandName in error.commands.skip(1)) {
214 215 216 217 218 219 220 221
        command = command.subcommands[commandName];
      }

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

222
  @override
223
  Future<void> run(Iterable<String> args) {
224
    // Have an invocation of 'build' print out it's sub-commands.
225
    // TODO(ianh): Move this to the Build command itself somehow.
226
    if (args.length == 1 && args.first == 'build') {
227
      args = <String>['build', '-h'];
228
    }
229

230
    return super.run(args);
Devon Carew's avatar
Devon Carew committed
231 232
  }

233
  @override
234
  Future<void> runCommand(ArgResults topLevelResults) async {
Jonah Williams's avatar
Jonah Williams committed
235
    final Map<Type, dynamic> contextOverrides = <Type, dynamic>{};
236

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262
    // 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,
    );

263
    if (topLevelResults['show-test-device'] as bool ||
264 265 266 267
        topLevelResults['device-id'] == FlutterTesterDevices.kTesterDeviceId) {
      FlutterTesterDevices.showFlutterTesterDevice = true;
    }

268 269
    // We must set Cache.flutterRoot early because other features use it (e.g.
    // enginePath's initializer uses it).
270
    final String flutterRoot = topLevelResults['flutter-root'] as String ?? defaultFlutterRoot;
271
    Cache.flutterRoot = globals.fs.path.normalize(globals.fs.path.absolute(flutterRoot));
272

273
    // Set up the tooling configuration.
274
    final String enginePath = await _findEnginePath(topLevelResults);
275
    if (enginePath != null) {
276
      contextOverrides.addAll(<Type, dynamic>{
277
        Artifacts: Artifacts.getLocalEngine(_findEngineBuildPath(topLevelResults, enginePath)),
278
      });
279 280
    }

281
    await context.run<void>(
282
      overrides: contextOverrides.map<Type, Generator>((Type type, dynamic value) {
283
        return MapEntry<Type, Generator>(type, () => value);
284 285
      }),
      body: () async {
286
        globals.logger.quiet = topLevelResults['quiet'] as bool;
287

288
        if (globals.platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true') {
289
          await Cache.lock();
290
        }
291

292
        if (topLevelResults['suppress-analytics'] as bool) {
293
          globals.flutterUsage.suppressAnalytics = true;
294
        }
295

296
        try {
297
          await globals.flutterVersion.ensureVersionFile();
298
        } on FileSystemException catch (e) {
299 300
          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.');
301 302
          throwToolExit('Failed to write the version file');
        }
303 304
        final bool machineFlag = topLevelResults['machine'] as bool;
        if (topLevelResults.command?.name != 'upgrade' && topLevelResults['version-check'] as bool && !machineFlag) {
305
          await globals.flutterVersion.checkFlutterVersionFreshness();
306 307
        }

308
        if (topLevelResults.wasParsed('packages')) {
309
          globalPackagesPath = globals.fs.path.normalize(globals.fs.path.absolute(topLevelResults['packages'] as String));
310
        }
311 312

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

315
        if (topLevelResults['version'] as bool) {
316
          globals.flutterUsage.sendCommand('version');
317
          globals.flutterVersion.fetchTagsAndUpdate();
318
          String status;
319
          if (machineFlag) {
320
            status = const JsonEncoder.withIndent('  ').convert(globals.flutterVersion.toJson());
321
          } else {
322
            status = globals.flutterVersion.toString();
323
          }
324
          globals.printStatus(status);
325 326 327
          return;
        }

328
        if (machineFlag) {
329 330 331 332 333
          throwToolExit('The --machine flag is only valid with the --version flag.', exitCode: 2);
        }
        await super.runCommand(topLevelResults);
      },
    );
Adam Barth's avatar
Adam Barth committed
334 335
  }

336
  String _tryEnginePath(String enginePath) {
337
    if (globals.fs.isDirectorySync(globals.fs.path.join(enginePath, 'out'))) {
338
      return enginePath;
339
    }
340 341 342
    return null;
  }

343
  Future<String> _findEnginePath(ArgResults globalResults) async {
344 345
    String engineSourcePath = globalResults['local-engine-src-path'] as String
      ?? globals.platform.environment[kFlutterEngineEnvironmentVariableName];
346

347
    if (engineSourcePath == null && globalResults['local-engine'] != null) {
348
      try {
349
        final PackageConfig packageConfig = await loadPackageConfigWithLogging(
350 351
          globals.fs.file(globalPackagesPath),
          logger: globals.logger,
352
          throwOnError: false,
353 354
        );
        Uri engineUri = packageConfig[kFlutterEnginePackageName]?.packageUriRoot;
355
        // Skip if sky_engine is the self-contained one.
356
        if (engineUri != null && globals.fs.identicalSync(globals.fs.path.join(Cache.flutterRoot, 'bin', 'cache', 'pkg', kFlutterEnginePackageName, 'lib'), engineUri.path)) {
357 358 359 360 361
          engineUri = null;
        }
        // If sky_engine is specified and the engineSourcePath not set, try to determine the engineSourcePath by sky_engine setting.
        // A typical engineUri looks like: file://flutter-engine-local-path/src/out/host_debug_unopt/gen/dart-pkg/sky_engine/lib/
        if (engineUri?.path != null) {
362 363
          engineSourcePath = globals.fs.directory(engineUri.path)?.parent?.parent?.parent?.parent?.parent?.parent?.path;
          if (engineSourcePath != null && (engineSourcePath == globals.fs.path.dirname(engineSourcePath) || engineSourcePath.isEmpty)) {
364
            engineSourcePath = null;
365 366 367
            throwToolExit(userMessages.runnerNoEngineSrcDir(kFlutterEnginePackageName, kFlutterEngineEnvironmentVariableName),
              exitCode: 2);
          }
368
        }
369 370 371 372 373
      } on FileSystemException {
        engineSourcePath = null;
      } on FormatException {
        engineSourcePath = null;
      }
374
      // If engineSourcePath is still not set, try to determine it by flutter root.
375
      engineSourcePath ??= _tryEnginePath(globals.fs.path.join(globals.fs.directory(Cache.flutterRoot).parent.path, 'engine', 'src'));
376
    }
377

378
    if (engineSourcePath != null && _tryEnginePath(engineSourcePath) == null) {
379
      throwToolExit(userMessages.runnerNoEngineBuildDirInPath(engineSourcePath),
380
        exitCode: 2);
381 382
    }

383 384 385
    return engineSourcePath;
  }

386 387 388 389 390 391 392
  String _getHostEngineBasename(String localEngineBasename) {
    // Determine the host engine directory associated with the local engine:
    // Strip '_sim_' since there are no host simulator builds.
    String tmpBasename = localEngineBasename.replaceFirst('_sim_', '_');
    tmpBasename = tmpBasename.substring(tmpBasename.indexOf('_') + 1);
    // Strip suffix for various archs.
    final List<String> suffixes = <String>['_arm', '_arm64', '_x86', '_x64'];
393
    for (final String suffix in suffixes) {
394 395 396 397 398
      tmpBasename = tmpBasename.replaceFirst(RegExp('$suffix\$'), '');
    }
    return 'host_' + tmpBasename;
  }

399
  EngineBuildPaths _findEngineBuildPath(ArgResults globalResults, String enginePath) {
400 401
    String localEngine;
    if (globalResults['local-engine'] != null) {
402
      localEngine = globalResults['local-engine'] as String;
403
    } else {
404
      throwToolExit(userMessages.runnerLocalEngineRequired, exitCode: 2);
405 406
    }

407 408
    final String engineBuildPath = globals.fs.path.normalize(globals.fs.path.join(enginePath, 'out', localEngine));
    if (!globals.fs.isDirectorySync(engineBuildPath)) {
409
      throwToolExit(userMessages.runnerNoEngineBuild(engineBuildPath), exitCode: 2);
410 411
    }

412
    final String basename = globals.fs.path.basename(engineBuildPath);
413
    final String hostBasename = _getHostEngineBasename(basename);
414 415
    final String engineHostBuildPath = globals.fs.path.normalize(globals.fs.path.join(globals.fs.path.dirname(engineBuildPath), hostBasename));
    if (!globals.fs.isDirectorySync(engineHostBuildPath)) {
416 417
      throwToolExit(userMessages.runnerNoEngineBuild(engineHostBuildPath), exitCode: 2);
    }
418

419
    return EngineBuildPaths(targetEngine: engineBuildPath, hostEngine: engineHostBuildPath);
420 421
  }

422
  @visibleForTesting
423 424 425 426
  static void initFlutterRoot() {
    Cache.flutterRoot ??= defaultFlutterRoot;
  }

427 428
  /// Get the root directories of the repo - the directories containing Dart packages.
  List<String> getRepoRoots() {
429
    final String root = globals.fs.path.absolute(Cache.flutterRoot);
430
    // not bin, and not the root
431
    return <String>['dev', 'examples', 'packages'].map<String>((String item) {
432
      return globals.fs.path.join(root, item);
433 434 435 436 437 438 439
    }).toList();
  }

  /// Get all pub packages in the Flutter repo.
  List<Directory> getRepoPackages() {
    return getRepoRoots()
      .expand<String>((String root) => _gatherProjectPaths(root))
440
      .map<Directory>((String dir) => globals.fs.directory(dir))
441 442 443 444
      .toList();
  }

  static List<String> _gatherProjectPaths(String rootPath) {
445
    if (globals.fs.isFileSync(globals.fs.path.join(rootPath, '.dartignore'))) {
446
      return <String>[];
447
    }
448 449


450
    final List<String> projectPaths = globals.fs.directory(rootPath)
451 452
      .listSync(followLinks: false)
      .expand((FileSystemEntity entity) {
453
        if (entity is Directory && !globals.fs.path.split(entity.path).contains('.dart_tool')) {
454 455 456
          return _gatherProjectPaths(entity.path);
        }
        return <String>[];
457 458
      })
      .toList();
459

460
    if (globals.fs.isFileSync(globals.fs.path.join(rootPath, 'pubspec.yaml'))) {
461
      projectPaths.add(rootPath);
462
    }
463 464

    return projectPaths;
465
  }
466
}