diff --git a/dev/bots/test.dart b/dev/bots/test.dart index d0e8014bec..a98d0efaef 100644 --- a/dev/bots/test.dart +++ b/dev/bots/test.dart @@ -279,6 +279,13 @@ Future _runToolTests() async { await selectSubshard(subshards); } +// Example apps that should not be built by _runBuildTests` +const List _excludedExampleApplications = [ + // This application contains no platform code and cannot be built, except for + // as a part of a '--fast-start' Android application. + 'splash', +]; + /// Verifies that AOT, APK, and IPA (if on macOS) builds the examples apps /// without crashing. It does not actually launch the apps. That happens later /// in the devicelab. This is just a smoke-test. In particular, this will verify @@ -290,6 +297,9 @@ Future _runBuildTests() async { if (fileEntity is! Directory) { continue; } + if (_excludedExampleApplications.any(fileEntity.path.endsWith)) { + continue; + } final String examplePath = fileEntity.path; await _flutterBuildAot(examplePath); await _flutterBuildApk(examplePath); @@ -763,6 +773,7 @@ Future _runHostOnlyDeviceLabTests() async { if (Platform.isMacOS) () => _runDevicelabTest('flutter_create_offline_test_mac'), if (Platform.isLinux) () => _runDevicelabTest('flutter_create_offline_test_linux'), if (Platform.isWindows) () => _runDevicelabTest('flutter_create_offline_test_windows'), + () => _runDevicelabTest('gradle_fast_start_test', environment: gradleEnvironment), // TODO(ianh): Fails on macOS looking for "dexdump", https://github.com/flutter/flutter/issues/42494 if (!Platform.isMacOS) () => _runDevicelabTest('gradle_jetifier_test', environment: gradleEnvironment), () => _runDevicelabTest('gradle_non_android_plugin_test', environment: gradleEnvironment), diff --git a/dev/devicelab/bin/tasks/gradle_fast_start_test.dart b/dev/devicelab/bin/tasks/gradle_fast_start_test.dart new file mode 100644 index 0000000000..73fdf12ea6 --- /dev/null +++ b/dev/devicelab/bin/tasks/gradle_fast_start_test.dart @@ -0,0 +1,42 @@ +// Copyright 2014 The Flutter 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:flutter_devicelab/framework/apk_utils.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; + +Future main() async { + await task(() async { + try { + await runPluginProjectTest((FlutterPluginProject pluginProject) async { + section('APK content for task assembleDebug with --fast-start'); + await pluginProject.runGradleTask('assembleDebug', + options: ['-Pfast-start=true']); + + final Iterable apkFiles = await getFilesInApk(pluginProject.debugApkPath); + + checkCollectionContains([ + ...debugAssets, + ...baseApkFiles, + 'lib/x86/libflutter.so', + 'lib/x86_64/libflutter.so', + 'lib/armeabi-v7a/libflutter.so', + 'lib/arm64-v8a/libflutter.so', + ], apkFiles); + + checkCollectionDoesNotContain([ + ...flutterAssets, + ], apkFiles); + }); + + return TaskResult.success(null); + } on TaskResult catch (taskResult) { + return taskResult; + } catch (e) { + return TaskResult.failure(e.toString()); + } + }); +} diff --git a/examples/splash/lib/main.dart b/examples/splash/lib/main.dart new file mode 100644 index 0000000000..3f5513683e --- /dev/null +++ b/examples/splash/lib/main.dart @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter 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 'package:flutter/material.dart'; + +void main() { + runApp( + const DecoratedBox( + decoration: BoxDecoration(color: Colors.white), + child: Center( + child: FlutterLogo(size: 48), + ), + ), + ); +} diff --git a/examples/splash/pubspec.yaml b/examples/splash/pubspec.yaml new file mode 100644 index 0000000000..40a17ca37e --- /dev/null +++ b/examples/splash/pubspec.yaml @@ -0,0 +1,41 @@ +name: splash + +environment: + # The pub client defaults to an <2.0.0 sdk constraint which we need to explicitly overwrite. + sdk: ">=2.0.0-dev.68.0 <3.0.0" + +dependencies: + flutter: + sdk: flutter + + collection: 1.14.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + meta: 1.1.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + typed_data: 1.1.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + vector_math: 2.0.8 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +dev_dependencies: + flutter_test: + sdk: flutter + + archive: 2.0.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + args: 1.5.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + async: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + boolean_selector: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + charcode: 1.1.2 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + convert: 2.1.1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + crypto: 2.1.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + image: 2.1.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + matcher: 0.12.6 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + path: 1.6.4 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + pedantic: 1.8.0+1 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + petitparser: 2.4.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + quiver: 2.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + source_span: 1.5.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stack_trace: 1.9.3 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + stream_channel: 2.0.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + string_scanner: 1.0.5 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + term_glyph: 1.1.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + test_api: 0.2.11 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + xml: 3.5.0 # THIS LINE IS AUTOGENERATED - TO UPDATE USE "flutter update-packages --force-upgrade" + +# PUBSPEC CHECKSUM: f789 diff --git a/examples/splash/test/splash_test.dart b/examples/splash/test/splash_test.dart new file mode 100644 index 0000000000..e1d597f621 --- /dev/null +++ b/examples/splash/test/splash_test.dart @@ -0,0 +1,16 @@ +// Copyright 2014 The Flutter 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 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:splash/main.dart' as entrypoint; + +void main() { + testWidgets('Displays flutter logo', (WidgetTester tester) async { + entrypoint.main(); + + expect(find.byType(FlutterLogo), findsOneWidget); + }); +} diff --git a/packages/flutter_tools/gradle/flutter.gradle b/packages/flutter_tools/gradle/flutter.gradle index fde4000583..e852f2fe24 100644 --- a/packages/flutter_tools/gradle/flutter.gradle +++ b/packages/flutter_tools/gradle/flutter.gradle @@ -472,6 +472,15 @@ class FlutterPlugin implements Plugin { return false } + /// Whether to build the debug app in "fast-start" mode. + private Boolean isFastStart() { + if (project.hasProperty("fast-start")) { + return project.property("fast-start").toBoolean() + } + return false + } + + private static Boolean shouldShrinkResources(Project project) { if (project.hasProperty("shrink")) { return project.property("shrink").toBoolean() @@ -610,6 +619,7 @@ class FlutterPlugin implements Plugin { localEngineSrcPath this.localEngineSrcPath targetPath target verbose isVerbose() + fastStart isFastStart() fileSystemRoots fileSystemRootsValue fileSystemScheme fileSystemSchemeValue trackWidgetCreation trackWidgetCreationValue @@ -729,6 +739,8 @@ abstract class BaseFlutterTask extends DefaultTask { String localEngine String localEngineSrcPath @Input + Boolean fastStart + @Input String targetPath @Optional Boolean verbose @@ -769,9 +781,13 @@ abstract class BaseFlutterTask extends DefaultTask { // cache. String[] ruleNames; if (buildMode == "debug") { - ruleNames = ["debug_android_application"] + if (fastStart) { + ruleNames = ["faststart_android_application"] + } else { + ruleNames = ["debug_android_application"] + } } else { - ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" } + ruleNames = targetPlatformValues.collect { "android_aot_bundle_${buildMode}_$it" } } project.exec { executable flutterExecutable.absolutePath @@ -788,7 +804,11 @@ abstract class BaseFlutterTask extends DefaultTask { args "assemble" args "--depfile", "${intermediateDir}/flutter_build.d" args "--output", "${intermediateDir}" - args "-dTargetFile=${targetPath}" + if (!fastStart || buildMode != "debug") { + args "-dTargetFile=${targetPath}" + } else { + args "-dTargetFile=${Paths.get(flutterRoot.absolutePath, "examples", "splash", "lib", "main.dart")}" + } args "-dTargetPlatform=android" args "-dBuildMode=${buildMode}" if (extraFrontEndOptions != null) { diff --git a/packages/flutter_tools/lib/src/android/android_device.dart b/packages/flutter_tools/lib/src/android/android_device.dart index 06788416cd..5a523f6b47 100644 --- a/packages/flutter_tools/lib/src/android/android_device.dart +++ b/packages/flutter_tools/lib/src/android/android_device.dart @@ -527,6 +527,7 @@ class AndroidDevice extends Device { androidBuildInfo: AndroidBuildInfo( debuggingOptions.buildInfo, targetArchs: [androidArch], + fastStart: debuggingOptions.fastStart ), ); // Package has been built, so we can get the updated application ID and @@ -643,6 +644,9 @@ class AndroidDevice extends Device { @override bool get supportsHotRestart => true; + @override + bool get supportsFastStart => true; + @override Future stopApp(AndroidApk app) { final List command = adbCommandForDevice(['shell', 'am', 'force-stop', app.id]); diff --git a/packages/flutter_tools/lib/src/android/gradle.dart b/packages/flutter_tools/lib/src/android/gradle.dart index 7f904c789e..4440d986e8 100644 --- a/packages/flutter_tools/lib/src/android/gradle.dart +++ b/packages/flutter_tools/lib/src/android/gradle.dart @@ -331,6 +331,9 @@ Future buildGradleApp({ // Don't use settings.gradle from the current project since it includes the plugins as subprojects. command.add('--settings-file=settings_aar.gradle'); } + if (androidBuildInfo.fastStart) { + command.add('-Pfast-start=true'); + } command.add(assembleTask); GradleHandledError detectedGradleError; diff --git a/packages/flutter_tools/lib/src/build_info.dart b/packages/flutter_tools/lib/src/build_info.dart index 775461addf..2f51d60f99 100644 --- a/packages/flutter_tools/lib/src/build_info.dart +++ b/packages/flutter_tools/lib/src/build_info.dart @@ -103,6 +103,7 @@ class AndroidBuildInfo { ], this.splitPerAbi = false, this.shrink = false, + this.fastStart = false, }); // The build info containing the mode and flavor. @@ -120,6 +121,9 @@ class AndroidBuildInfo { /// The target platforms for the build. final Iterable targetArchs; + + /// Whether to bootstrap an empty application. + final bool fastStart; } /// A summary of the compilation strategy used for Dart. diff --git a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart index 36f1898daa..b13f632391 100644 --- a/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart +++ b/packages/flutter_tools/lib/src/build_runner/resident_web_runner.dart @@ -423,7 +423,7 @@ class _ExperimentalResidentWebRunner extends ResidentWebRunner { @override Future restart({ bool fullRestart = false, - bool pauseAfterRestart = false, + bool pause = false, String reason, bool benchmarkMode = false, }) async { @@ -692,7 +692,7 @@ class _DwdsResidentWebRunner extends ResidentWebRunner { @override Future restart({ bool fullRestart = false, - bool pauseAfterRestart = false, + bool pause = false, String reason, bool benchmarkMode = false, }) async { diff --git a/packages/flutter_tools/lib/src/build_system/targets/android.dart b/packages/flutter_tools/lib/src/build_system/targets/android.dart index 5a030fb1fe..1798d95e3a 100644 --- a/packages/flutter_tools/lib/src/build_system/targets/android.dart +++ b/packages/flutter_tools/lib/src/build_system/targets/android.dart @@ -31,10 +31,13 @@ abstract class AndroidAssetBundle extends Target { List get outputs => const []; @override - List get depfiles => const [ - 'flutter_assets.d', + List get depfiles => [ + if (_copyAssets) + 'flutter_assets.d', ]; + bool get _copyAssets => true; + @override Future build(Environment environment) async { if (environment.defines[kBuildMode] == null) { @@ -56,8 +59,10 @@ abstract class AndroidAssetBundle extends Target { fs.file(isolateSnapshotData) .copySync(outputDirectory.childFile('isolate_snapshot_data').path); } - final Depfile assetDepfile = await copyAssets(environment, outputDirectory); - assetDepfile.writeToFile(environment.buildDir.childFile('flutter_assets.d')); + if (_copyAssets) { + final Depfile assetDepfile = await copyAssets(environment, outputDirectory); + assetDepfile.writeToFile(environment.buildDir.childFile('flutter_assets.d')); + } } @override @@ -90,6 +95,17 @@ class DebugAndroidApplication extends AndroidAssetBundle { ]; } +/// A minimal android application that does not include assets. +class FastStartAndroidApplication extends DebugAndroidApplication { + const FastStartAndroidApplication(); + + @override + String get name => 'faststart_android_application'; + + @override + bool get _copyAssets => false; +} + /// An implementation of [AndroidAssetBundle] that only includes assets. class AotAndroidAssetBundle extends AndroidAssetBundle { const AotAndroidAssetBundle(); diff --git a/packages/flutter_tools/lib/src/commands/assemble.dart b/packages/flutter_tools/lib/src/commands/assemble.dart index 74856e33fc..5265b5c89e 100644 --- a/packages/flutter_tools/lib/src/commands/assemble.dart +++ b/packages/flutter_tools/lib/src/commands/assemble.dart @@ -37,6 +37,7 @@ const List _kDefaultTargets = [ DebugBundleLinuxAssets(), WebReleaseBundle(), DebugAndroidApplication(), + FastStartAndroidApplication(), ProfileAndroidApplication(), ReleaseAndroidApplication(), // These are one-off rules for bundle and aot compat diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index 5c1b9db3ed..a53b373c06 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -578,7 +578,7 @@ class AppDomain extends Domain { } _inProgressHotReload = app._runInZone(this, () { - return app.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart, reason: restartReason); + return app.restart(fullRestart: fullRestart, pause: pauseAfterRestart, reason: restartReason); }); return _inProgressHotReload.whenComplete(() { _inProgressHotReload = null; @@ -945,8 +945,8 @@ class AppInstance { _AppRunLogger _logger; - Future restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) { - return runner.restart(fullRestart: fullRestart, pauseAfterRestart: pauseAfterRestart, reason: reason); + Future restart({ bool fullRestart = false, bool pause = false, String reason }) { + return runner.restart(fullRestart: fullRestart, pause: pause, reason: reason); } Future reloadMethod({ String classId, String libraryId }) { diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index fb13cb7480..222f207816 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -187,6 +187,14 @@ class RunCommand extends RunCommandBase { hide: true, help: 'Whether to automatically invoke webOnlyInitializePlatform.', ) + ..addFlag('fast-start', + negatable: true, + defaultsTo: false, + hide: true, + help: 'Whether to quickly bootstrap applications with a minimal app. ' + 'Currently this is only supported on Android devices. This option ' + 'cannot be paired with --use-application-binary.' + ) ..addOption(FlutterOptions.kExtraFrontEndOptions, hide: true) ..addOption(FlutterOptions.kExtraGenSnapshotOptions, hide: true) ..addMultiOption(FlutterOptions.kEnableExperiment, @@ -284,6 +292,11 @@ class RunCommand extends RunCommandBase { if (!runningWithPrebuiltApplication) { await super.validateCommand(); } + + if (boolArg('fast-start') && runningWithPrebuiltApplication) { + throwToolExit('--fast-start is not supported with --use-application-binary'); + } + devices = await findAllTargetDevices(); if (devices == null) { throwToolExit(null); @@ -322,6 +335,9 @@ class RunCommand extends RunCommandBase { hostname: featureFlags.isWebEnabled ? stringArg('web-hostname') : '', port: featureFlags.isWebEnabled ? stringArg('web-port') : '', vmserviceOutFile: stringArg('vmservice-out-file'), + // Allow forcing fast-start to off to prevent doing more work on devices that + // don't support it. + fastStart: boolArg('fast-start') && devices.every((Device device) => device.supportsFastStart), ); } } @@ -384,6 +400,12 @@ class RunCommand extends RunCommandBase { } for (Device device in devices) { + if (!device.supportsFastStart && boolArg('fast-start')) { + printStatus( + 'Using --fast-start option with device ${device.name}, but this device ' + 'does not support it. Overriding the setting to false.' + ); + } if (await device.isLocalEmulator) { if (await device.supportsHardwareRendering) { final bool enableSoftwareRendering = boolArg('enable-software-rendering') == true; diff --git a/packages/flutter_tools/lib/src/device.dart b/packages/flutter_tools/lib/src/device.dart index 2b081ba5f1..b335d3e3ad 100644 --- a/packages/flutter_tools/lib/src/device.dart +++ b/packages/flutter_tools/lib/src/device.dart @@ -420,6 +420,9 @@ abstract class Device { /// application. bool get supportsScreenshot => false; + /// Whether the device supports the '--fast-start' development mode. + bool get supportsFastStart => false; + /// Stop an app package on the current device. Future stopApp(covariant ApplicationPackage app); @@ -534,6 +537,7 @@ class DebuggingOptions { this.hostname, this.port, this.vmserviceOutFile, + this.fastStart = false, }) : debuggingEnabled = true; DebuggingOptions.disabled(this.buildInfo, { this.initializePlatform = true, this.port, this.hostname, this.cacheSkSL = false, }) @@ -550,7 +554,8 @@ class DebuggingOptions { verboseSystemLogs = false, hostVmServicePort = null, deviceVmServicePort = null, - vmserviceOutFile = null; + vmserviceOutFile = null, + fastStart = false; final bool debuggingEnabled; @@ -574,6 +579,7 @@ class DebuggingOptions { final String hostname; /// A file where the vmservice URL should be written after the application is started. final String vmserviceOutFile; + final bool fastStart; bool get hasObservatoryPort => hostVmServicePort != null; } diff --git a/packages/flutter_tools/lib/src/resident_runner.dart b/packages/flutter_tools/lib/src/resident_runner.dart index 0402b83f0f..e666af6c13 100644 --- a/packages/flutter_tools/lib/src/resident_runner.dart +++ b/packages/flutter_tools/lib/src/resident_runner.dart @@ -714,7 +714,7 @@ abstract class ResidentRunner { bool get supportsRestart => false; - Future restart({ bool fullRestart = false, bool pauseAfterRestart = false, String reason }) { + Future restart({ bool fullRestart = false, bool pause = false, String reason }) { final String mode = isRunningProfile ? 'profile' : isRunningRelease ? 'release' : 'this'; throw '${fullRestart ? 'Restart' : 'Reload'} is not supported in $mode mode'; diff --git a/packages/flutter_tools/lib/src/run_hot.dart b/packages/flutter_tools/lib/src/run_hot.dart index 9629942893..a3fbe18bff 100644 --- a/packages/flutter_tools/lib/src/run_hot.dart +++ b/packages/flutter_tools/lib/src/run_hot.dart @@ -103,7 +103,7 @@ class HotRunner extends ResidentRunner { bool pause = false, }) async { // TODO(cbernaschina): check that isolateId is the id of the UI isolate. - final OperationResult result = await restart(pauseAfterRestart: pause); + final OperationResult result = await restart(pause: pause); if (!result.isOk) { throw rpc.RpcException( rpc_error_code.INTERNAL_ERROR, @@ -114,7 +114,7 @@ class HotRunner extends ResidentRunner { Future _restartService({ bool pause = false }) async { final OperationResult result = - await restart(fullRestart: true, pauseAfterRestart: pause); + await restart(fullRestart: true, pause: pause); if (!result.isOk) { throw rpc.RpcException( rpc_error_code.INTERNAL_ERROR, @@ -263,6 +263,18 @@ class HotRunner extends ResidentRunner { } } + // In fast-start mode, apps are initialized from a placeholder splashscreen + // app. We must do a restart here to load the program and assets for the + // real app. + if (debuggingOptions.fastStart) { + await restart( + fullRestart: true, + benchmarkMode: !debuggingOptions.startPaused, + reason: 'restart', + silent: true, + ); + } + appStartedCompleter?.complete(); if (benchmarkMode) { @@ -408,12 +420,10 @@ class HotRunner extends ResidentRunner { Uri packagesUri, Uri assetsDirectoryUri, ) { - final List> futures = >[ - for (FlutterView view in device.views) view.runFromSource(entryUri, packagesUri, assetsDirectoryUri), - ]; - final Completer completer = Completer(); - Future.wait(futures).whenComplete(() { completer.complete(null); }); - return completer.future; + return Future.wait(>[ + for (FlutterView view in device.views) + view.runFromSource(entryUri, packagesUri, assetsDirectoryUri), + ]); } Future _launchFromDevFS(String mainScript) async { @@ -510,9 +520,11 @@ class HotRunner extends ResidentRunner { for (FlutterDevice device in flutterDevices) { for (FlutterView view in device.views) { isolateNotifications.add( - view.owner.vm.vmService.onIsolateEvent.then((Stream serviceEvents) async { + view.owner.vm.vmService.onIsolateEvent + .then((Stream serviceEvents) async { await for (ServiceEvent serviceEvent in serviceEvents) { - if (serviceEvent.owner.name.contains('_spawn') && serviceEvent.kind == ServiceEvent.kIsolateExit) { + if (serviceEvent.owner.name.contains('_spawn') + && serviceEvent.kind == ServiceEvent.kIsolateExit) { return; } } @@ -573,9 +585,10 @@ class HotRunner extends ResidentRunner { @override Future restart({ bool fullRestart = false, - bool pauseAfterRestart = false, String reason, bool benchmarkMode = false, + bool silent = false, + bool pause = false, }) async { String targetPlatform; String sdkName; @@ -602,8 +615,11 @@ class HotRunner extends ResidentRunner { emulator: emulator, reason: reason, benchmarkMode: benchmarkMode, + silent: silent, ); - printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.'); + if (!silent) { + printStatus('Restarted application in ${getElapsedAsMilliseconds(timer.elapsed)}.'); + } return result; } final OperationResult result = await _hotReloadHelper( @@ -611,11 +627,13 @@ class HotRunner extends ResidentRunner { sdkName: sdkName, emulator: emulator, reason: reason, - pauseAfterRestart: pauseAfterRestart, + pause: pause, ); if (result.isOk) { final String elapsed = getElapsedAsMilliseconds(timer.elapsed); - printStatus('${result.message} in $elapsed.'); + if (!silent) { + printStatus('${result.message} in $elapsed.'); + } } return result; } @@ -626,15 +644,19 @@ class HotRunner extends ResidentRunner { bool emulator, String reason, bool benchmarkMode, + bool silent, }) async { if (!canHotRestart) { return OperationResult(1, 'hotRestart not supported'); } - final Status status = logger.startProgress( - 'Performing hot restart...', - timeout: timeoutConfiguration.fastOperation, - progressId: 'hot.restart', - ); + Status status; + if (!silent) { + status = logger.startProgress( + 'Performing hot restart...', + timeout: timeoutConfiguration.fastOperation, + progressId: 'hot.restart', + ); + } OperationResult result; String restartEvent = 'restart'; try { @@ -662,7 +684,7 @@ class HotRunner extends ResidentRunner { emulator: emulator, fullRestart: true, reason: reason).send(); - status.cancel(); + status?.cancel(); } return result; } @@ -672,7 +694,7 @@ class HotRunner extends ResidentRunner { String sdkName, bool emulator, String reason, - bool pauseAfterRestart = false, + bool pause, }) async { final bool reloadOnTopOfSnapshot = _runningFromSnapshot; final String progressPrefix = reloadOnTopOfSnapshot ? 'Initializing' : 'Performing'; @@ -687,8 +709,8 @@ class HotRunner extends ResidentRunner { targetPlatform: targetPlatform, sdkName: sdkName, emulator: emulator, - pause: pauseAfterRestart, reason: reason, + pause: pause, onSlow: (String message) { status?.cancel(); status = logger.startProgress( diff --git a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart index 87a0a15d14..fad57ed7c4 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/run_test.dart @@ -11,12 +11,15 @@ import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/artifacts.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/context.dart'; +import 'package:flutter_tools/src/base/file_system.dart'; +import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/base/user_messages.dart'; import 'package:flutter_tools/src/build_info.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/run.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/features.dart'; +import 'package:flutter_tools/src/globals.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/reporting/reporting.dart'; import 'package:flutter_tools/src/resident_runner.dart'; @@ -56,6 +59,72 @@ void main() { } }); + testUsingContext('does not support "--use-application-binary" and "--fast-start"', () async { + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + fs.file('pubspec.yaml').createSync(); + fs.file('.packages').createSync(); + + final RunCommand command = RunCommand(); + applyMocksToCommand(command); + try { + await createTestCommandRunner(command).run([ + 'run', + '--use-application-binary=app/bar/faz', + '--fast-start', + '--no-pub', + '--show-test-device', + ]); + fail('Expect exception'); + } catch (e) { + expect(e.toString(), contains('--fast-start is not supported with --use-application-binary')); + } + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('Forces fast start off for devices that do not support it', () async { + final MockDevice mockDevice = MockDevice(TargetPlatform.android_arm); + when(mockDevice.name).thenReturn('mockdevice'); + when(mockDevice.supportsFastStart).thenReturn(false); + when(mockDevice.supportsHotReload).thenReturn(true); + when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async => false); + when(deviceManager.hasSpecifiedAllDevices).thenReturn(false); + when(deviceManager.findTargetDevices(any)).thenAnswer((Invocation invocation) { + return Future>.value([mockDevice]); + }); + when(deviceManager.getDevices()).thenAnswer((Invocation invocation) { + return Stream.value(mockDevice); + }); + fs.file(fs.path.join('lib', 'main.dart')).createSync(recursive: true); + fs.file('pubspec.yaml').createSync(); + fs.file('.packages').createSync(); + + final RunCommand command = RunCommand(); + applyMocksToCommand(command); + try { + await createTestCommandRunner(command).run([ + 'run', + '--fast-start', + '--no-pub', + ]); + fail('Expect exception'); + } catch (e) { + expect(e, isInstanceOf()); + } + + final BufferLogger bufferLogger = logger as BufferLogger; + expect(bufferLogger.statusText, contains( + 'Using --fast-start option with device mockdevice, but this device ' + 'does not support it. Overriding the setting to false.' + )); + }, overrides: { + FileSystem: () => MemoryFileSystem(), + ProcessManager: () => FakeProcessManager.any(), + DeviceManager: () => MockDeviceManager(), + }); + + group('run app', () { MemoryFileSystem fs; MockArtifacts mockArtifacts; @@ -166,6 +235,7 @@ void main() { final MockDevice mockDevice = MockDevice(TargetPlatform.ios); when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) => Future.value(false)); when(mockDevice.getLogReader(app: anyNamed('app'))).thenReturn(MockDeviceLogReader()); + when(mockDevice.supportsFastStart).thenReturn(true); // App fails to start because we're only interested in usage when(mockDevice.startApp( any, @@ -497,6 +567,9 @@ class FakeDevice extends Fake implements Device { @override bool get supportsHotReload => false; + @override + bool get supportsFastStart => false; + @override Future get sdkNameAndVersion => Future.value(''); diff --git a/packages/flutter_tools/test/general.shard/resident_runner_test.dart b/packages/flutter_tools/test/general.shard/resident_runner_test.dart index 39b55c69e5..0dd418d614 100644 --- a/packages/flutter_tools/test/general.shard/resident_runner_test.dart +++ b/packages/flutter_tools/test/general.shard/resident_runner_test.dart @@ -154,6 +154,41 @@ void main() { expect(onAppStart.isCompleted, true); })); + test('ResidentRunner can attach to device successfully with --fast-start', () => testbed.run(() async { + when(mockDevice.supportsHotRestart).thenReturn(true); + when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { + return 'Example'; + }); + when(mockDevice.targetPlatform).thenAnswer((Invocation invocation) async { + return TargetPlatform.android_arm; + }); + when(mockDevice.isLocalEmulator).thenAnswer((Invocation invocation) async { + return false; + }); + residentRunner = HotRunner( + [ + mockFlutterDevice, + ], + stayResident: false, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, fastStart: true, startPaused: true), + ); + final Completer onConnectionInfo = Completer.sync(); + final Completer onAppStart = Completer.sync(); + final Future result = residentRunner.attach( + appStartedCompleter: onAppStart, + connectionInfoCompleter: onConnectionInfo, + ); + final Future connectionInfo = onConnectionInfo.future; + + expect(await result, 0); + + verify(mockFlutterDevice.initLogReader()).called(1); + + expect(onConnectionInfo.isCompleted, true); + expect((await connectionInfo).baseUri, 'foo://bar'); + expect(onAppStart.isCompleted, true); + })); + test('ResidentRunner can handle an RPC exception from hot reload', () => testbed.run(() async { when(mockDevice.sdkNameAndVersion).thenAnswer((Invocation invocation) async { return 'Example'; diff --git a/packages/flutter_tools/test/integration.shard/test_driver.dart b/packages/flutter_tools/test/integration.shard/test_driver.dart index 4279b7a452..ff881e1853 100644 --- a/packages/flutter_tools/test/integration.shard/test_driver.dart +++ b/packages/flutter_tools/test/integration.shard/test_driver.dart @@ -497,7 +497,7 @@ class FlutterRunTestDriver extends FlutterTestDriver { // fast. unawaited(_process.exitCode.then((_) { if (!prematureExitGuard.isCompleted) { - prematureExitGuard.completeError('Process existed prematurely: ${args.join(' ')}'); + prematureExitGuard.completeError('Process existed prematurely: ${args.join(' ')}: $_errorBuffer'); } }));