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

5 6
// @dart = 2.8

7 8
import 'package:meta/meta.dart';

9
import '../base/common.dart';
10
import '../base/io.dart';
11
import '../base/os.dart';
12
import '../base/process.dart';
13
import '../base/time.dart';
14
import '../cache.dart';
Devon Carew's avatar
Devon Carew committed
15
import '../dart/pub.dart';
16
import '../globals_null_migrated.dart' as globals;
17
import '../runner/flutter_command.dart';
18
import '../version.dart';
19 20

class UpgradeCommand extends FlutterCommand {
21 22 23 24
  UpgradeCommand({
    @required bool verboseHelp,
    UpgradeCommandRunner commandRunner,
  })
25
    : _commandRunner = commandRunner ?? UpgradeCommandRunner() {
26 27 28 29 30 31 32 33 34
    argParser
      ..addFlag(
        'force',
        abbr: 'f',
        help: 'Force upgrade the flutter branch, potentially discarding local changes.',
        negatable: false,
      )
      ..addFlag(
        'continue',
35
        hide: !verboseHelp,
36
        negatable: false,
37 38 39 40
        help: 'Trigger the second half of the upgrade flow. This should not be invoked '
              'manually. It is used re-entrantly by the standard upgrade command after '
              'the new version of Flutter is available, to hand off the upgrade process '
              'from the old version to the new version.',
41 42 43
      )
      ..addOption(
        'working-directory',
44 45 46
        hide: !verboseHelp,
        help: 'Override the upgrade working directory. '
              'This is only intended to enable integration testing of the tool itself.'
47 48 49
      )
      ..addFlag(
        'verify-only',
50
        help: 'Checks for any new flutter updates, without actually fetching them.',
51
        negatable: false,
52
      );
53 54
  }

55 56
  final UpgradeCommandRunner _commandRunner;

57
  @override
58
  final String name = 'upgrade';
59 60

  @override
61 62
  final String description = 'Upgrade your copy of Flutter.';

63 64 65
  @override
  bool get shouldUpdateCache => false;

66
  @override
67
  Future<FlutterCommandResult> runCommand() {
68
    _commandRunner.workingDirectory = stringArg('working-directory') ?? Cache.flutterRoot;
69
    return _commandRunner.runCommand(
70 71 72
      force: boolArg('force'),
      continueFlow: boolArg('continue'),
      testFlow: stringArg('working-directory') != null,
73
      gitTagVersion: GitTagVersion.determine(globals.processUtils),
74 75
      flutterVersion: stringArg('working-directory') == null
        ? globals.flutterVersion
76
        : FlutterVersion(clock: const SystemClock(), workingDirectory: _commandRunner.workingDirectory),
77
      verifyOnly: boolArg('verify-only'),
78
    );
79 80 81 82 83
  }
}

@visibleForTesting
class UpgradeCommandRunner {
84 85 86 87 88 89 90 91 92

  String workingDirectory;

  Future<FlutterCommandResult> runCommand({
    @required bool force,
    @required bool continueFlow,
    @required bool testFlow,
    @required GitTagVersion gitTagVersion,
    @required FlutterVersion flutterVersion,
93
    @required bool verifyOnly,
94
  }) async {
95
    if (!continueFlow) {
96 97 98 99 100
      await runCommandFirstHalf(
        force: force,
        gitTagVersion: gitTagVersion,
        flutterVersion: flutterVersion,
        testFlow: testFlow,
101
        verifyOnly: verifyOnly,
102
      );
103 104 105
    } else {
      await runCommandSecondHalf(flutterVersion);
    }
106
    return FlutterCommandResult.success();
107 108
  }

109 110 111 112 113
  Future<void> runCommandFirstHalf({
    @required bool force,
    @required GitTagVersion gitTagVersion,
    @required FlutterVersion flutterVersion,
    @required bool testFlow,
114
    @required bool verifyOnly,
115
  }) async {
116
    final FlutterVersion upstreamVersion = await fetchLatestVersion();
117
    if (flutterVersion.frameworkRevision == upstreamVersion.frameworkRevision) {
118 119 120
      globals.printStatus('Flutter is already up to date on channel ${flutterVersion.channel}');
      globals.printStatus('$flutterVersion');
      return;
121 122
    } else if (verifyOnly) {
      globals.printStatus('A new version of Flutter is available on channel ${flutterVersion.channel}\n');
123 124
      globals.printStatus('The latest version: ${upstreamVersion.frameworkVersion} (revision ${upstreamVersion.frameworkRevisionShort})', emphasis: true);
      globals.printStatus('Your current version: ${flutterVersion.frameworkVersion} (revision ${flutterVersion.frameworkRevisionShort})\n');
125 126 127 128 129 130
      globals.printStatus('To upgrade now, run "flutter upgrade".');
      if (flutterVersion.channel == 'stable') {
        globals.printStatus('\nSee the announcement and release notes:');
        globals.printStatus('https://flutter.dev/docs/development/tools/sdk/release-notes');
      }
      return;
131
    }
132 133 134
    if (!force && gitTagVersion == const GitTagVersion.unknown()) {
      // If the commit is a recognized branch and not master,
      // explain that we are avoiding potential damage.
135
      if (flutterVersion.channel != 'master' && kOfficialChannels.contains(flutterVersion.channel)) {
136 137 138 139 140 141 142 143 144 145
        throwToolExit(
          'Unknown flutter tag. Abandoning upgrade to avoid destroying local '
          'changes. It is recommended to use git directly if not working on '
          'an official channel.'
        );
      // Otherwise explain that local changes can be lost.
      } else {
        throwToolExit(
          'Unknown flutter tag. Abandoning upgrade to avoid destroying local '
          'changes. If it is okay to remove local changes, then re-run this '
146
          'command with "--force".'
147 148 149
        );
      }
    }
Chris Bracken's avatar
Chris Bracken committed
150
    // If there are uncommitted changes we might be on the right commit but
151
    // we should still warn.
152
    if (!force && await hasUncommittedChanges()) {
153 154 155 156 157
      throwToolExit(
        'Your flutter checkout has local changes that would be erased by '
        'upgrading. If you want to keep these changes, it is recommended that '
        'you stash them via "git stash" or else commit the changes to a local '
        'branch. If it is okay to remove local changes, then re-run this '
158
        'command with "--force".'
159 160
      );
    }
161
    recordState(flutterVersion);
162
    globals.printStatus('Upgrading Flutter to ${upstreamVersion.frameworkVersion} from ${flutterVersion.frameworkVersion} in $workingDirectory...');
163
    await attemptReset(upstreamVersion.frameworkRevision);
164
    if (!testFlow) {
165 166
      await flutterUpgradeContinue();
    }
167 168
  }

169 170 171 172 173 174 175 176
  void recordState(FlutterVersion flutterVersion) {
    final Channel channel = getChannelForName(flutterVersion.channel);
    if (channel == null) {
      return;
    }
    globals.persistentToolState.updateLastActiveVersion(flutterVersion.frameworkRevision, channel);
  }

177
  Future<void> flutterUpgradeContinue() async {
178
    final int code = await globals.processUtils.stream(
179
      <String>[
180
        globals.fs.path.join('bin', 'flutter'),
181 182 183 184
        'upgrade',
        '--continue',
        '--no-version-check',
      ],
185
      workingDirectory: workingDirectory,
186
      allowReentrantFlutter: true,
187
      environment: Map<String, String>.of(globals.platform.environment),
188 189 190 191 192 193 194 195 196
    );
    if (code != 0) {
      throwToolExit(null, exitCode: code);
    }
  }

  // This method should only be called if the upgrade command is invoked
  // re-entrantly with the `--continue` flag
  Future<void> runCommandSecondHalf(FlutterVersion flutterVersion) async {
197
    // Make sure the welcome message re-display is delayed until the end.
198
    globals.persistentToolState.redisplayWelcomeMessage = false;
199 200 201
    await precacheArtifacts();
    await updatePackages(flutterVersion);
    await runDoctor();
202
    // Force the welcome message to re-display following the upgrade.
203
    globals.persistentToolState.redisplayWelcomeMessage = true;
204 205
  }

206
  Future<bool> hasUncommittedChanges() async {
207
    try {
208
      final RunResult result = await globals.processUtils.run(
209 210
        <String>['git', 'status', '-s'],
        throwOnError: true,
211
        workingDirectory: workingDirectory,
212
      );
213
      return result.stdout.trim().isNotEmpty;
214 215 216
    } on ProcessException catch (error) {
      throwToolExit(
        'The tool could not verify the status of the current flutter checkout. '
217 218
        'This might be due to git not being installed or an internal error. '
        'If it is okay to ignore potential local changes, then re-run this '
219 220
        'command with "--force".\n'
        'Error: $error.'
221
      );
222 223 224
    }
  }

225
  /// Returns the remote HEAD flutter version.
226
  ///
227 228
  /// Exits tool if there is no upstream.
  Future<FlutterVersion> fetchLatestVersion() async {
229
    String revision;
230
    try {
231
      // Fetch upstream branch's commits and tags
232
      await globals.processUtils.run(
233
        <String>['git', 'fetch', '--tags'],
234
        throwOnError: true,
235
        workingDirectory: workingDirectory,
236
      );
237
      // '@{u}' means upstream HEAD
238
      final RunResult result = await globals.processUtils.run(
239 240 241 242 243
          <String>[ 'git', 'rev-parse', '--verify', '@{u}'],
          throwOnError: true,
          workingDirectory: workingDirectory,
      );
      revision = result.stdout.trim();
244 245 246 247
    } on Exception catch (e) {
      final String errorString = e.toString();
      if (errorString.contains('fatal: HEAD does not point to a branch')) {
        throwToolExit(
248 249 250 251
          'You are not currently on a release branch. Use git to '
          "check out an official branch ('stable', 'beta', 'dev', or 'master') "
          'and retry, for example:\n'
          '  git checkout stable'
252 253 254
        );
      } else if (errorString.contains('fatal: no upstream configured for branch')) {
        throwToolExit(
255 256 257
          'Unable to upgrade Flutter: no origin repository configured. '
          "Run 'git remote add origin "
          "https://github.com/flutter/flutter' in $workingDirectory");
258 259 260
      } else {
        throwToolExit(errorString);
      }
261
    }
262
    return FlutterVersion(workingDirectory: workingDirectory, frameworkRevision: revision);
263
  }
264

265
  /// Attempts a hard reset to the given revision.
266
  ///
267 268 269
  /// This is a reset instead of fast forward because if we are on a release
  /// branch with cherry picks, there may not be a direct fast-forward route
  /// to the next release.
270 271
  Future<void> attemptReset(String newRevision) async {
    try {
272
      await globals.processUtils.run(
273 274 275 276 277 278
        <String>['git', 'reset', '--hard', newRevision],
        throwOnError: true,
        workingDirectory: workingDirectory,
      );
    } on ProcessException catch (e) {
      throwToolExit(e.message, exitCode: e.errorCode);
279
    }
280
  }
281

282 283 284 285 286 287
  /// Update the engine repository and precache all artifacts.
  ///
  /// Check for and download any engine and pkg/ updates. We run the 'flutter'
  /// shell script re-entrantly here so that it will download the updated
  /// Dart and so forth if necessary.
  Future<void> precacheArtifacts() async {
288 289
    globals.printStatus('');
    globals.printStatus('Upgrading engine...');
290
    final int code = await globals.processUtils.stream(
291
      <String>[
292
        globals.fs.path.join('bin', 'flutter'), '--no-color', '--no-version-check', 'precache',
293
      ],
294
      workingDirectory: workingDirectory,
295
      allowReentrantFlutter: true,
296
      environment: Map<String, String>.of(globals.platform.environment),
297
    );
298 299 300 301
    if (code != 0) {
      throwToolExit(null, exitCode: code);
    }
  }
302

303 304
  /// Update the user's packages.
  Future<void> updatePackages(FlutterVersion flutterVersion) async {
305 306
    globals.printStatus('');
    globals.printStatus(flutterVersion.toString());
307
    final String projectRoot = findProjectRoot(globals.fs);
308
    if (projectRoot != null) {
309
      globals.printStatus('');
310 311 312 313 314 315
      await pub.get(
        context: PubContext.pubUpgrade,
        directory: projectRoot,
        upgrade: true,
        generateSyntheticPackage: false,
      );
Devon Carew's avatar
Devon Carew committed
316
    }
317
  }
318

319 320
  /// Run flutter doctor in case requirements have changed.
  Future<void> runDoctor() async {
321 322
    globals.printStatus('');
    globals.printStatus('Running flutter doctor...');
323
    await globals.processUtils.stream(
324
      <String>[
325
        globals.fs.path.join('bin', 'flutter'), '--no-version-check', 'doctor',
326
      ],
327
      workingDirectory: workingDirectory,
328 329
      allowReentrantFlutter: true,
    );
330 331
  }
}