Skip to content

Commit 8a31a3a

Browse files
Flutter preview device (#135639)
Fixes flutter/flutter#130277 This PR does two things: 1. introduce a hidden `flutter build _preview` command, that will build a debug windows desktop app and copy it into the SDK's binary cache. This command is only intended to be run during packaging. 2. introduce a new device type, called `PreviewDevice`, which relies on the prebuilt desktop debug app from step 1, copies it into the target app's assets build folder, and then hot reloads their dart code into it.
1 parent 492b6f7 commit 8a31a3a

24 files changed

+1465
-656
lines changed

packages/flutter_tools/lib/executable.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,9 +173,11 @@ List<FlutterCommand> generateCommands({
173173
fileSystem: globals.fs,
174174
),
175175
BuildCommand(
176+
artifacts: globals.artifacts!,
176177
fileSystem: globals.fs,
177178
buildSystem: globals.buildSystem,
178179
osUtils: globals.os,
180+
processUtils: globals.processUtils,
179181
verboseHelp: verboseHelp,
180182
androidSdk: globals.androidSdk,
181183
logger: globals.logger,

packages/flutter_tools/lib/src/artifacts.dart

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,9 @@ enum Artifact {
6969

7070
/// The location of file generators.
7171
flutterToolsFileGenerators,
72+
73+
/// Pre-built desktop debug app.
74+
flutterPreviewDevice,
7275
}
7376

7477
/// A subset of [Artifact]s that are platform and build mode independent
@@ -213,6 +216,8 @@ String? _artifactToFileName(Artifact artifact, Platform hostPlatform, [ BuildMod
213216
return 'const_finder.dart.snapshot';
214217
case Artifact.flutterToolsFileGenerators:
215218
return '';
219+
case Artifact.flutterPreviewDevice:
220+
return 'flutter_preview$exe';
216221
}
217222
}
218223

@@ -573,6 +578,7 @@ class CachedArtifacts implements Artifacts {
573578
case Artifact.windowsCppClientWrapper:
574579
case Artifact.windowsDesktopPath:
575580
case Artifact.flutterToolsFileGenerators:
581+
case Artifact.flutterPreviewDevice:
576582
return _getHostArtifactPath(artifact, platform, mode);
577583
}
578584
}
@@ -612,6 +618,7 @@ class CachedArtifacts implements Artifacts {
612618
case Artifact.windowsCppClientWrapper:
613619
case Artifact.windowsDesktopPath:
614620
case Artifact.flutterToolsFileGenerators:
621+
case Artifact.flutterPreviewDevice:
615622
return _getHostArtifactPath(artifact, platform, mode);
616623
}
617624
}
@@ -663,6 +670,7 @@ class CachedArtifacts implements Artifacts {
663670
case Artifact.windowsCppClientWrapper:
664671
case Artifact.windowsDesktopPath:
665672
case Artifact.flutterToolsFileGenerators:
673+
case Artifact.flutterPreviewDevice:
666674
return _getHostArtifactPath(artifact, platform, mode);
667675
}
668676
}
@@ -745,6 +753,9 @@ class CachedArtifacts implements Artifacts {
745753
throw StateError('Artifact $artifact not available for platform $platform.');
746754
case Artifact.flutterToolsFileGenerators:
747755
return _getFileGeneratorsPath();
756+
case Artifact.flutterPreviewDevice:
757+
assert(platform == TargetPlatform.windows_x64);
758+
return _cache.getArtifactDirectory('flutter_preview').childFile('flutter_preview.exe').path;
748759
}
749760
}
750761

@@ -1016,6 +1027,8 @@ class CachedLocalEngineArtifacts implements Artifacts {
10161027
return _fileSystem.path.join(_getDartSdkPath(), 'bin', 'utils', artifactFileName);
10171028
case Artifact.flutterToolsFileGenerators:
10181029
return _getFileGeneratorsPath();
1030+
case Artifact.flutterPreviewDevice:
1031+
throw UnimplementedError('The preview device is not supported with local engine builds');
10191032
}
10201033
}
10211034

@@ -1118,7 +1131,6 @@ class CachedLocalWebSdkArtifacts implements Artifacts {
11181131
_platform = platform,
11191132
_operatingSystemUtils = operatingSystemUtils;
11201133

1121-
11221134
final Artifacts _parent;
11231135
final String _webSdkPath;
11241136
final FileSystem _fileSystem;
@@ -1169,6 +1181,7 @@ class CachedLocalWebSdkArtifacts implements Artifacts {
11691181
case Artifact.fontSubset:
11701182
case Artifact.constFinder:
11711183
case Artifact.flutterToolsFileGenerators:
1184+
case Artifact.flutterPreviewDevice:
11721185
break;
11731186
}
11741187
}

packages/flutter_tools/lib/src/commands/build.dart

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
import 'package:meta/meta.dart';
66

77
import '../android/android_sdk.dart';
8+
import '../artifacts.dart';
89
import '../base/file_system.dart';
910
import '../base/logger.dart';
1011
import '../base/os.dart';
12+
import '../base/process.dart';
1113
import '../build_info.dart';
1214
import '../build_system/build_system.dart';
15+
import '../cache.dart';
1316
import '../commands/build_linux.dart';
1417
import '../commands/build_macos.dart';
1518
import '../commands/build_windows.dart';
@@ -21,15 +24,18 @@ import 'build_bundle.dart';
2124
import 'build_ios.dart';
2225
import 'build_ios_framework.dart';
2326
import 'build_macos_framework.dart';
27+
import 'build_preview.dart';
2428
import 'build_web.dart';
2529

2630
class BuildCommand extends FlutterCommand {
2731
BuildCommand({
32+
required Artifacts artifacts,
2833
required FileSystem fileSystem,
2934
required BuildSystem buildSystem,
3035
required OperatingSystemUtils osUtils,
3136
required Logger logger,
3237
required AndroidSdk? androidSdk,
38+
required ProcessUtils processUtils,
3339
bool verboseHelp = false,
3440
}){
3541
_addSubcommand(
@@ -67,6 +73,14 @@ class BuildCommand extends FlutterCommand {
6773
verboseHelp: verboseHelp
6874
));
6975
_addSubcommand(BuildWindowsCommand(logger: logger, verboseHelp: verboseHelp));
76+
_addSubcommand(BuildPreviewCommand(
77+
artifacts: artifacts,
78+
flutterRoot: Cache.flutterRoot!,
79+
fs: fileSystem,
80+
logger: logger,
81+
processUtils: processUtils,
82+
verboseHelp: verboseHelp,
83+
));
7084
}
7185

7286
void _addSubcommand(BuildSubCommand command) {
@@ -90,14 +104,15 @@ class BuildCommand extends FlutterCommand {
90104

91105
abstract class BuildSubCommand extends FlutterCommand {
92106
BuildSubCommand({
93-
required Logger logger,
107+
required this.logger,
94108
required bool verboseHelp
95-
}): _logger = logger {
109+
}) {
96110
requiresPubspecYaml();
97111
usesFatalWarningsOption(verboseHelp: verboseHelp);
98112
}
99113

100-
final Logger _logger;
114+
@protected
115+
final Logger logger;
101116

102117
@override
103118
bool get reportNullSafety => true;
@@ -111,15 +126,15 @@ abstract class BuildSubCommand extends FlutterCommand {
111126
@protected
112127
void displayNullSafetyMode(BuildInfo buildInfo) {
113128
if (buildInfo.nullSafetyMode != NullSafetyMode.sound) {
114-
_logger.printStatus('');
115-
_logger.printStatus(
129+
logger.printStatus('');
130+
logger.printStatus(
116131
'Building without sound null safety ⚠️',
117132
emphasis: true,
118133
);
119-
_logger.printStatus(
134+
logger.printStatus(
120135
'Dart 3 will only support sound null safety, see https://dart.dev/null-safety',
121136
);
122137
}
123-
_logger.printStatus('');
138+
logger.printStatus('');
124139
}
125140
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:file/file.dart';
6+
7+
import '../artifacts.dart';
8+
import '../base/common.dart';
9+
import '../base/io.dart';
10+
import '../base/process.dart';
11+
import '../build_info.dart';
12+
import '../cache.dart';
13+
import '../globals.dart' as globals;
14+
import '../project.dart';
15+
import '../runner/flutter_command.dart' show FlutterCommandResult;
16+
import '../windows/build_windows.dart';
17+
import 'build.dart';
18+
19+
class BuildPreviewCommand extends BuildSubCommand {
20+
BuildPreviewCommand({
21+
required super.logger,
22+
required bool verboseHelp,
23+
required this.fs,
24+
required this.flutterRoot,
25+
required this.processUtils,
26+
required this.artifacts,
27+
}) : super(verboseHelp: verboseHelp) {
28+
addCommonDesktopBuildOptions(verboseHelp: verboseHelp);
29+
}
30+
31+
@override
32+
final String name = '_preview';
33+
34+
@override
35+
final bool hidden = true;
36+
37+
@override
38+
Future<Set<DevelopmentArtifact>> get requiredArtifacts async => <DevelopmentArtifact>{
39+
DevelopmentArtifact.windows,
40+
};
41+
42+
@override
43+
final String description = 'Build Flutter preview (desktop) app.';
44+
45+
final FileSystem fs;
46+
final String flutterRoot;
47+
final ProcessUtils processUtils;
48+
final Artifacts artifacts;
49+
50+
static const BuildInfo buildInfo = BuildInfo(
51+
BuildMode.debug,
52+
null, // no flavor
53+
// users may add icons later
54+
treeShakeIcons: false,
55+
);
56+
57+
@override
58+
void requiresPubspecYaml() {}
59+
60+
static const String appName = 'flutter_preview';
61+
62+
@override
63+
Future<FlutterCommandResult> runCommand() async {
64+
if (!globals.platform.isWindows) {
65+
throwToolExit('"build _preview" is currently only supported on Windows hosts.');
66+
}
67+
final Directory targetDir = fs.systemTempDirectory.createTempSync('flutter-build-preview');
68+
try {
69+
final FlutterProject flutterProject = await _createProject(targetDir);
70+
await buildWindows(
71+
flutterProject.windows,
72+
buildInfo,
73+
);
74+
75+
final File previewDevice = targetDir
76+
.childDirectory(getWindowsBuildDirectory(TargetPlatform.windows_x64))
77+
.childDirectory('runner')
78+
.childDirectory('Debug')
79+
.childFile('$appName.exe');
80+
if (!previewDevice.existsSync()) {
81+
throw StateError('Preview device not found at ${previewDevice.absolute.path}');
82+
}
83+
final String newPath = artifacts.getArtifactPath(Artifact.flutterPreviewDevice);
84+
fs.file(newPath).parent.createSync(recursive: true);
85+
previewDevice.copySync(newPath);
86+
return FlutterCommandResult.success();
87+
} finally {
88+
try {
89+
targetDir.deleteSync(recursive: true);
90+
} on FileSystemException catch (exception) {
91+
logger.printError('Failed to delete ${targetDir.path}\n\n$exception');
92+
}
93+
}
94+
}
95+
96+
Future<FlutterProject> _createProject(Directory targetDir) async {
97+
final List<String> cmd = <String>[
98+
fs.path.join(flutterRoot, 'bin', 'flutter.bat'),
99+
'create',
100+
'--empty',
101+
'--project-name',
102+
'flutter_preview',
103+
targetDir.path,
104+
];
105+
final RunResult result = await processUtils.run(
106+
cmd,
107+
allowReentrantFlutter: true,
108+
);
109+
if (result.exitCode != 0) {
110+
final StringBuffer buffer = StringBuffer('${cmd.join(' ')} exited with code ${result.exitCode}\n');
111+
buffer.writeln('stdout:\n${result.stdout}\n');
112+
buffer.writeln('stderr:\n${result.stderr}');
113+
throw ProcessException(cmd.first, cmd.sublist(1), buffer.toString(), result.exitCode);
114+
}
115+
return FlutterProject.fromDirectory(targetDir);
116+
}
117+
}

packages/flutter_tools/lib/src/features.dart

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ abstract class FeatureFlags {
5353
/// Whether native assets compilation and bundling is enabled.
5454
bool get isNativeAssetsEnabled => false;
5555

56+
/// Whether native assets compilation and bundling is enabled.
57+
bool get isPreviewDeviceEnabled => true;
58+
5659
/// Whether a particular feature is enabled for the current channel.
5760
///
5861
/// Prefer using one of the specific getters above instead of this API.
@@ -72,6 +75,7 @@ const List<Feature> allFeatures = <Feature>[
7275
flutterWebWasm,
7376
cliAnimation,
7477
nativeAssets,
78+
previewDevice,
7579
];
7680

7781
/// All current Flutter feature flags that can be configured.
@@ -172,6 +176,19 @@ const Feature nativeAssets = Feature(
172176
),
173177
);
174178

179+
/// Enable Flutter preview prebuilt device.
180+
const Feature previewDevice = Feature(
181+
name: 'Flutter preview prebuilt device',
182+
configSetting: 'enable-flutter-preview',
183+
environmentOverride: 'FLUTTER_PREVIEW_DEVICE',
184+
master: FeatureChannelSetting(
185+
available: true,
186+
),
187+
beta: FeatureChannelSetting(
188+
available: true,
189+
),
190+
);
191+
175192
/// A [Feature] is a process for conditionally enabling tool features.
176193
///
177194
/// All settings are optional, and if not provided will generally default to

packages/flutter_tools/lib/src/flutter_device_manager.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import 'macos/macos_device.dart';
2727
import 'macos/macos_ipad_device.dart';
2828
import 'macos/macos_workflow.dart';
2929
import 'macos/xcdevice.dart';
30+
import 'preview_device.dart';
3031
import 'tester/flutter_tester.dart';
3132
import 'version.dart';
3233
import 'web/web_device.dart';
@@ -104,6 +105,14 @@ class FlutterDeviceManager extends DeviceManager {
104105
fileSystem: fileSystem,
105106
operatingSystemUtils: operatingSystemUtils,
106107
),
108+
PreviewDeviceDiscovery(
109+
platform: platform,
110+
artifacts: artifacts,
111+
fileSystem: fileSystem,
112+
logger: logger,
113+
processManager: processManager,
114+
featureFlags: featureFlags,
115+
),
107116
LinuxDevices(
108117
platform: platform,
109118
featureFlags: featureFlags,

packages/flutter_tools/lib/src/flutter_features.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ class FlutterFeatureFlags implements FeatureFlags {
5858
@override
5959
bool get isNativeAssetsEnabled => isEnabled(nativeAssets);
6060

61+
@override
62+
bool get isPreviewDeviceEnabled => isEnabled(previewDevice);
63+
6164
@override
6265
bool isEnabled(Feature feature) {
6366
final String currentChannel = _flutterVersion.channel;

0 commit comments

Comments
 (0)