flutter_command_runner.dart 18.1 KB
Newer Older
1 2 3 4 5 6 7 8 9
// Copyright 2015 The Chromium Authors. All rights reserved.
// 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';

10
import '../android/android_sdk.dart';
11
import '../artifacts.dart';
12
import '../base/common.dart';
13
import '../base/context.dart';
14
import '../base/file_system.dart';
Devon Carew's avatar
Devon Carew committed
15
import '../base/logger.dart';
16
import '../base/os.dart';
17
import '../base/platform.dart';
18
import '../base/process.dart';
19
import '../base/process_manager.dart';
20
import '../base/utils.dart';
21
import '../cache.dart';
22
import '../dart/package_map.dart';
23
import '../device.dart';
24
import '../globals.dart';
25
import '../usage.dart';
26
import '../version.dart';
27
import '../vmservice.dart';
28

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

35
class FlutterCommandRunner extends CommandRunner<Null> {
Devon Carew's avatar
Devon Carew committed
36 37
  FlutterCommandRunner({ bool verboseHelp: false }) : super(
    'flutter',
38 39 40 41 42 43 44 45 46 47
    'Manage your Flutter app development.\n'
      '\n'
      'Common actions:\n'
      '\n'
      '  flutter create <output directory>\n'
      '    Create a new Flutter project in the specified directory.\n'
      '\n'
      '  flutter run [options]\n'
      '    Run your Flutter application on an attached device\n'
      '    or in an emulator.',
Devon Carew's avatar
Devon Carew committed
48
  ) {
49 50 51 52
    argParser.addFlag('verbose',
        abbr: 'v',
        negatable: false,
        help: 'Noisy logging, including all shell commands executed.');
53 54 55 56
    argParser.addFlag('quiet',
        negatable: false,
        hide: !verboseHelp,
        help: 'Reduce the amount of output from some commands.');
Devon Carew's avatar
Devon Carew committed
57 58
    argParser.addOption('device-id',
        abbr: 'd',
59
        help: 'Target device id or name (prefixes allowed).');
60 61 62
    argParser.addFlag('version',
        negatable: false,
        help: 'Reports the version of this tool.');
63 64 65 66
    argParser.addFlag('color',
        negatable: true,
        hide: !verboseHelp,
        help: 'Whether to use terminal colors.');
67 68 69 70
    argParser.addFlag('suppress-analytics',
        negatable: false,
        hide: !verboseHelp,
        help: 'Suppress analytics reporting when this command runs.');
71 72
    argParser.addFlag('bug-report',
        negatable: false,
73 74 75
        help:
            'Captures a bug report file to submit to the Flutter team '
            '(contains local paths, device identifiers, and log snippets).');
Devon Carew's avatar
Devon Carew committed
76

Ian Hickson's avatar
Ian Hickson committed
77
    String packagesHelp;
78
    if (fs.isFileSync(kPackagesFileName))
79
      packagesHelp = '\n(defaults to "$kPackagesFileName")';
Ian Hickson's avatar
Ian Hickson committed
80
    else
81
      packagesHelp = '\n(required, since the current directory does not contain a "$kPackagesFileName" file)';
82
    argParser.addOption('packages',
83
        hide: !verboseHelp,
84
        help: 'Path to your ".packages" file.$packagesHelp');
Ian Hickson's avatar
Ian Hickson committed
85
    argParser.addOption('flutter-root',
86
        help: 'The root directory of the Flutter repository (uses \$$kFlutterRootEnvironmentVariableName if set).');
Ian Hickson's avatar
Ian Hickson committed
87

Devon Carew's avatar
Devon Carew committed
88 89
    if (verboseHelp)
      argParser.addSeparator('Local build selection options (not normally required):');
90

91
    argParser.addOption('local-engine-src-path',
Devon Carew's avatar
Devon Carew committed
92
        hide: !verboseHelp,
93
        help:
Ian Hickson's avatar
Ian Hickson committed
94 95 96 97
            'Path to your engine src directory, if you are building Flutter locally.\n'
            'Defaults to \$$kFlutterEngineEnvironmentVariableName if set, otherwise defaults to the path given in your pubspec.yaml\n'
            'dependency_overrides for $kFlutterEnginePackageName, if any, or, failing that, tries to guess at the location\n'
            'based on the value of the --flutter-root option.');
98

99 100 101 102 103
    argParser.addOption('local-engine',
        hide: !verboseHelp,
        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'
104
            'This path is relative to --local-engine-src-path/out.');
105
    argParser.addOption('record-to',
106
        hide: true,
107
        help:
108
            'Enables recording of process invocations (including stdout and stderr of all such invocations),\n'
109 110
            'and file system access (reads and writes).\n'
            'Serializes that recording to a directory with the path specified in this flag. If the\n'
111
            'directory does not already exist, it will be created.');
112
    argParser.addOption('replay-from',
113
        hide: true,
114
        help:
115
            'Enables mocking of process invocations by replaying their stdout, stderr, and exit code from\n'
116 117 118
            'the specified recording (obtained via --record-to). The path specified in this flag must refer\n'
            'to a directory that holds serialized process invocations structured according to the output of\n'
            '--record-to.');
119 120
  }

121
  @override
Hixie's avatar
Hixie committed
122
  String get usageFooter {
123
    return 'Run "flutter help -v" for verbose help output, including less commonly used options.';
Hixie's avatar
Hixie committed
124
  }
Devon Carew's avatar
Devon Carew committed
125

126
  static String get _defaultFlutterRoot {
127 128
    if (platform.environment.containsKey(kFlutterRootEnvironmentVariableName))
      return platform.environment[kFlutterRootEnvironmentVariableName];
Devon Carew's avatar
Devon Carew committed
129
    try {
130
      if (platform.script.scheme == 'data')
Hixie's avatar
Hixie committed
131
        return '../..'; // we're running as a test
132
      final String script = platform.script.toFilePath();
133 134 135 136
      if (fs.path.basename(script) == kSnapshotFileName)
        return fs.path.dirname(fs.path.dirname(fs.path.dirname(script)));
      if (fs.path.basename(script) == kFlutterToolsScriptFileName)
        return fs.path.dirname(fs.path.dirname(fs.path.dirname(fs.path.dirname(script))));
137 138 139 140 141 142

      // If run from a bare script within the repo.
      if (script.contains('flutter/packages/'))
        return script.substring(0, script.indexOf('flutter/packages/') + 8);
      if (script.contains('flutter/examples/'))
        return script.substring(0, script.indexOf('flutter/examples/') + 8);
Devon Carew's avatar
Devon Carew committed
143
    } catch (error) {
Hixie's avatar
Hixie committed
144 145 146
      // we don't have a logger at the time this is run
      // (which is why we don't use printTrace here)
      print('Unable to locate flutter root: $error');
Devon Carew's avatar
Devon Carew committed
147
    }
Ian Hickson's avatar
Ian Hickson committed
148 149 150
    return '.';
  }

151
  @override
152
  Future<Null> run(Iterable<String> args) {
153 154 155 156
    // Have an invocation of 'build' print out it's sub-commands.
    if (args.length == 1 && args.first == 'build')
      args = <String>['build', '-h'];

157
    return super.run(args);
Devon Carew's avatar
Devon Carew committed
158 159
  }

160
  @override
161
  Future<Null> runCommand(ArgResults globalResults) async {
162
    // Check for verbose.
163 164 165 166
    if (globalResults['verbose']) {
      // Override the logger.
      context.setVariable(Logger, new VerboseLogger());
    }
167

168 169 170 171 172
    String recordTo = globalResults['record-to'];
    String replayFrom = globalResults['replay-from'];

    if (globalResults['bug-report']) {
      // --bug-report implies --record-to=<tmp_path>
173
      final Directory tmp = await const LocalFileSystem()
174 175 176 177 178
          .systemTempDirectory
          .createTemp('flutter_tools_');
      recordTo = tmp.path;

      // Record the arguments that were used to invoke this runner.
179 180
      final File manifest = tmp.childFile('MANIFEST.txt');
      final StringBuffer buffer = new StringBuffer()
181 182 183 184 185 186 187 188 189
        ..writeln('# arguments')
        ..writeln(globalResults.arguments)
        ..writeln()
        ..writeln('# rest')
        ..writeln(globalResults.rest);
      await manifest.writeAsString(buffer.toString(), flush: true);

      // ZIP the recording up once the recording has been serialized.
      addShutdownHook(() async {
190
        final File zipFile = getUniqueFile(fs.currentDirectory, 'bugreport', 'zip');
191
        os.zip(tmp, zipFile);
192 193 194 195
        printStatus(
            'Bug report written to ${zipFile.basename}.\n'
            'Note that this bug report contains local paths, device '
            'identifiers, and log snippets.');
196 197 198 199 200
      }, ShutdownStage.POST_PROCESS_RECORDING);
      addShutdownHook(() => tmp.delete(recursive: true), ShutdownStage.CLEANUP);
    }

    assert(recordTo == null || replayFrom == null);
201

202 203
    if (recordTo != null) {
      recordTo = recordTo.trim();
204 205 206 207 208
      if (recordTo.isEmpty)
        throwToolExit('record-to location not specified');
      enableRecordingProcessManager(recordTo);
      enableRecordingFileSystem(recordTo);
      await enableRecordingPlatform(recordTo);
209
      VMService.enableRecordingConnection(recordTo);
210 211
    }

212 213
    if (replayFrom != null) {
      replayFrom = replayFrom.trim();
214 215 216 217 218
      if (replayFrom.isEmpty)
        throwToolExit('replay-from location not specified');
      await enableReplayProcessManager(replayFrom);
      enableReplayFileSystem(replayFrom);
      await enableReplayPlatform(replayFrom);
219
      VMService.enableReplayConnection(replayFrom);
220 221
    }

222 223
    logger.quiet = globalResults['quiet'];

224 225
    if (globalResults.wasParsed('color'))
      logger.supportsColor = globalResults['color'];
226

227
    // We must set Cache.flutterRoot early because other features use it (e.g.
228 229 230
    // enginePath's initializer uses it).
    final String flutterRoot = globalResults['flutter-root'] ?? _defaultFlutterRoot;
    Cache.flutterRoot = fs.path.normalize(fs.path.absolute(flutterRoot));
231

232
    if (platform.environment['FLUTTER_ALREADY_LOCKED'] != 'true')
233
      await Cache.lock();
234

235 236 237
    if (globalResults['suppress-analytics'])
      flutterUsage.suppressAnalytics = true;

238
    _checkFlutterCopy();
239

240
    if (globalResults.wasParsed('packages'))
241
      PackageMap.globalPackagesPath = fs.path.normalize(fs.path.absolute(globalResults['packages']));
Hixie's avatar
Hixie committed
242

243 244
    // See if the user specified a specific device.
    deviceManager.specifiedDeviceId = globalResults['device-id'];
245

246
    // Set up the tooling configuration.
247
    final String enginePath = _findEnginePath(globalResults);
248
    if (enginePath != null) {
249
      Artifacts.useLocalEngine(enginePath, _findEngineBuildPath(globalResults, enginePath));
250 251
    }

252
    // The Android SDK could already have been set by tests.
253
    context.putIfAbsent(AndroidSdk, AndroidSdk.locateAndroidSdk);
254

255
    if (globalResults['version']) {
256
      flutterUsage.sendCommand('version');
257
      printStatus(FlutterVersion.instance.toString());
258
      return;
259 260
    }

261
    await super.runCommand(globalResults);
Adam Barth's avatar
Adam Barth committed
262 263
  }

264
  String _tryEnginePath(String enginePath) {
265
    if (fs.isDirectorySync(fs.path.join(enginePath, 'out')))
266 267 268 269
      return enginePath;
    return null;
  }

270
  String _findEnginePath(ArgResults globalResults) {
271
    String engineSourcePath = globalResults['local-engine-src-path'] ?? platform.environment[kFlutterEngineEnvironmentVariableName];
272

273
    if (engineSourcePath == null && globalResults['local-engine'] != null) {
274
      try {
275
        final Uri engineUri = new PackageMap(PackageMap.globalPackagesPath).map[kFlutterEnginePackageName];
276
        if (engineUri != null) {
277
          engineSourcePath = fs.path.dirname(fs.path.dirname(fs.path.dirname(fs.path.dirname(engineUri.path))));
278
          final bool dirExists = fs.isDirectorySync(fs.path.join(engineSourcePath, 'out'));
279 280 281
          if (engineSourcePath == '/' || engineSourcePath.isEmpty || !dirExists)
            engineSourcePath = null;
        }
282
      } on FileSystemException { } on FormatException { }
283 284

      if (engineSourcePath == null)
285
        engineSourcePath = _tryEnginePath(fs.path.join(Cache.flutterRoot, '../engine/src'));
286

287
      if (engineSourcePath == null) {
288
        printError('Unable to detect local Flutter engine build directory.\n'
Ian Hickson's avatar
Ian Hickson committed
289 290
            'Either specify a dependency_override for the $kFlutterEnginePackageName package in your pubspec.yaml and\n'
            'ensure --package-root is set if necessary, or set the \$$kFlutterEngineEnvironmentVariableName environment variable, or\n'
291
            'use --local-engine-src-path to specify the path to the root of your flutter/engine repository.');
292
        throw new ProcessExit(2);
293
      }
294
    }
295

296
    if (engineSourcePath != null && _tryEnginePath(engineSourcePath) == null) {
297
      printError('Unable to detect a Flutter engine build directory in $engineSourcePath.\n'
298 299 300 301 302
          'Please ensure that $engineSourcePath is a Flutter engine \'src\' directory and that\n'
          'you have compiled the engine in that directory, which should produce an \'out\' directory');
      throw new ProcessExit(2);
    }

303 304 305
    return engineSourcePath;
  }

306
  String _findEngineBuildPath(ArgResults globalResults, String enginePath) {
307 308 309 310
    String localEngine;
    if (globalResults['local-engine'] != null) {
      localEngine = globalResults['local-engine'];
    } else {
311 312
      printError('You must specify --local-engine if you are using a locally built engine.');
      throw new ProcessExit(2);
313 314
    }

315
    final String engineBuildPath = fs.path.normalize(fs.path.join(enginePath, 'out', localEngine));
316
    if (!fs.isDirectorySync(engineBuildPath)) {
317 318 319 320 321 322 323
      printError('No Flutter engine build found at $engineBuildPath.');
      throw new ProcessExit(2);
    }

    return engineBuildPath;
  }

324
  static void initFlutterRoot() {
325 326
    if (Cache.flutterRoot == null)
      Cache.flutterRoot = _defaultFlutterRoot;
327
  }
328 329 330

  /// Get all pub packages in the Flutter repo.
  List<Directory> getRepoPackages() {
331
    return _gatherProjectPaths(fs.path.absolute(Cache.flutterRoot))
332
      .map((String dir) => fs.directory(dir))
333 334 335 336
      .toList();
  }

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

340
    if (fs.isFileSync(fs.path.join(rootPath, 'pubspec.yaml')))
341 342
      return <String>[rootPath];

343
    return fs.directory(rootPath)
344 345 346
      .listSync(followLinks: false)
      .expand((FileSystemEntity entity) {
        return entity is Directory ? _gatherProjectPaths(entity.path) : <String>[];
347 348
      })
      .toList();
349
  }
350

351 352
  /// Get the entry-points we want to analyze in the Flutter repo.
  List<Directory> getRepoAnalysisEntryPoints() {
353
    final String rootPath = fs.path.absolute(Cache.flutterRoot);
354
    final List<Directory> result = <Directory>[
355
      // not bin, and not the root
356 357
      fs.directory(fs.path.join(rootPath, 'dev')),
      fs.directory(fs.path.join(rootPath, 'examples')),
358
    ];
359 360
    // And since analyzer refuses to look at paths that end in "packages/":
    result.addAll(
361
      _gatherProjectPaths(fs.path.join(rootPath, 'packages'))
362
      .map<Directory>((String path) => fs.directory(path))
363 364
    );
    return result;
365 366
  }

367
  void _checkFlutterCopy() {
368 369
    // If the current directory is contained by a flutter repo, check that it's
    // the same flutter that is currently running.
370
    String directory = fs.path.normalize(fs.path.absolute(fs.currentDirectory.path));
371 372 373 374

    // Check if the cwd is a flutter dir.
    while (directory.isNotEmpty) {
      if (_isDirectoryFlutterRepo(directory)) {
375
        if (!_compareResolvedPaths(directory, Cache.flutterRoot)) {
376
          printError(
377 378
            'Warning: the \'flutter\' tool you are currently running is not the one from the current directory:\n'
            '  running Flutter  : ${Cache.flutterRoot}\n'
379
            '  current directory: $directory\n'
380 381
            'This can happen when you have multiple copies of flutter installed. Please check your system path to verify\n'
            'that you\'re running the expected version (run \'flutter --version\' to see which flutter is on your path).\n'
382 383 384 385 386 387
          );
        }

        break;
      }

388
      final String parent = fs.path.dirname(directory);
389 390 391 392
      if (parent == directory)
        break;
      directory = parent;
    }
393 394

    // Check that the flutter running is that same as the one referenced in the pubspec.
395
    if (fs.isFileSync(kPackagesFileName)) {
396 397
      final PackageMap packageMap = new PackageMap(kPackagesFileName);
      final Uri flutterUri = packageMap.map['flutter'];
398 399 400

      if (flutterUri != null && (flutterUri.scheme == 'file' || flutterUri.scheme == '')) {
        // .../flutter/packages/flutter/lib
401 402
        final Uri rootUri = flutterUri.resolve('../../..');
        final String flutterPath = fs.path.normalize(fs.file(rootUri).absolute.path);
403

404
        if (!fs.isDirectorySync(flutterPath)) {
405
          printError(
406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425
            'Warning! This package referenced a Flutter repository via the .packages file that is\n'
            'no longer available. The repository from which the \'flutter\' tool is currently\n'
            'executing will be used instead.\n'
            '  running Flutter tool: ${Cache.flutterRoot}\n'
            '  previous reference  : $flutterPath\n'
            'This can happen if you deleted or moved your copy of the Flutter repository, or\n'
            'if it was on a volume that is no longer mounted or has been mounted at a\n'
            'different location. Please check your system path to verify that you are running\n'
            'the expected version (run \'flutter --version\' to see which flutter is on your path).\n'
          );
        } else if (!_compareResolvedPaths(flutterPath, Cache.flutterRoot)) {
          printError(
            'Warning! The \'flutter\' tool you are currently running is from a different Flutter\n'
            'repository than the one last used by this package. The repository from which the\n'
            '\'flutter\' tool is currently executing will be used instead.\n'
            '  running Flutter tool: ${Cache.flutterRoot}\n'
            '  previous reference  : $flutterPath\n'
            'This can happen when you have multiple copies of flutter installed. Please check\n'
            'your system path to verify that you are running the expected version (run\n'
            '\'flutter --version\' to see which flutter is on your path).\n'
426 427 428 429
          );
        }
      }
    }
430 431 432 433 434
  }

  // Check if `bin/flutter` and `bin/cache/engine.stamp` exist.
  bool _isDirectoryFlutterRepo(String directory) {
    return
435 436
      fs.isFileSync(fs.path.join(directory, 'bin/flutter')) &&
      fs.isFileSync(fs.path.join(directory, 'bin/cache/engine.stamp'));
437
  }
438
}
439 440

bool _compareResolvedPaths(String path1, String path2) {
441 442
  path1 = fs.directory(fs.path.absolute(path1)).resolveSymbolicLinksSync();
  path2 = fs.directory(fs.path.absolute(path2)).resolveSymbolicLinksSync();
443 444 445

  return path1 == path2;
}