diff --git a/.ci.yaml b/.ci.yaml index 384fc42d31..b4e902ea69 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -1497,7 +1497,10 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:35v1"}, - {"dependency": "open_jdk", "version": "version:17"} + {"dependency": "open_jdk", "version": "version:17"}, + {"dependency": "clang", "version": "git_revision:5d5aba78dbbee75508f01bcaa69aedb2ab79065a"}, + {"dependency": "cmake", "version": "build_id:8787856497187628321"}, + {"dependency": "ninja", "version": "version:1.9.0"} ] shard: tool_tests subshard: commands @@ -6322,7 +6325,8 @@ targets: dependencies: >- [ {"dependency": "android_sdk", "version": "version:35v1"}, - {"dependency": "open_jdk", "version": "version:17"} + {"dependency": "open_jdk", "version": "version:17"}, + {"dependency": "vs_build", "version": "version:vs2019"} ] shard: tool_tests subshard: commands diff --git a/packages/flutter_tools/lib/executable.dart b/packages/flutter_tools/lib/executable.dart index 8d888ced94..bd6d7b7ced 100644 --- a/packages/flutter_tools/lib/executable.dart +++ b/packages/flutter_tools/lib/executable.dart @@ -85,7 +85,10 @@ Future main(List args) async { final bool daemon = args.contains('daemon'); final bool runMachine = (args.contains('--machine') && args.contains('run')) || - (args.contains('--machine') && args.contains('attach')); + (args.contains('--machine') && args.contains('attach')) || + // `flutter widget-preview start` starts an application that requires a logger + // to be setup for machine mode. + (args.contains('widget-preview') && args.contains('start')); // Cache.flutterRoot must be set early because other features use it (e.g. // enginePath's initializer uses it). This can only work with the real @@ -247,10 +250,14 @@ List generateCommands({required bool verboseHelp, required bool nativeAssetsBuilder: globals.nativeAssetsBuilder, ), WidgetPreviewCommand( + verboseHelp: verboseHelp, logger: globals.logger, fs: globals.fs, projectFactory: globals.projectFactory, cache: globals.cache, + platform: globals.platform, + shutdownHooks: globals.shutdownHooks, + os: globals.os, ), UpgradeCommand(verboseHelp: verboseHelp), SymbolizeCommand(stdio: globals.stdio, fileSystem: globals.fs), diff --git a/packages/flutter_tools/lib/src/commands/daemon.dart b/packages/flutter_tools/lib/src/commands/daemon.dart index cbb0a37cac..e7621fcd1c 100644 --- a/packages/flutter_tools/lib/src/commands/daemon.dart +++ b/packages/flutter_tools/lib/src/commands/daemon.dart @@ -195,6 +195,21 @@ class Daemon { ); } + factory Daemon.createMachineDaemon() { + final Daemon daemon = Daemon( + DaemonConnection( + daemonStreams: DaemonStreams.fromStdio(globals.stdio, logger: globals.logger), + logger: globals.logger, + ), + notifyingLogger: + (globals.logger is NotifyingLogger) + ? globals.logger as NotifyingLogger + : NotifyingLogger(verbose: globals.logger.isVerbose, parent: globals.logger), + logToStdout: true, + ); + return daemon; + } + final DaemonConnection connection; late DaemonDomain daemonDomain; @@ -809,15 +824,11 @@ class AppDomain extends Domain { ); } final Completer appStartedCompleter = Completer(); - // We don't want to wait for this future to complete, and callbacks won't fail, - // as it just writes to stdout. - unawaited( - appStartedCompleter.future.then((void value) { - _sendAppEvent(app, 'started'); - }), - ); - await app._runInZone(this, () async { + // This future won't complete until the application has shutdown, so we don't want to + // await it. However, we do need to listen to the future in order to handle possible + // tool exits + final Future appRunFuture = app._runInZone(this, () async { try { await runOrAttach( connectionInfoCompleter: connectionInfoCompleter, @@ -836,6 +847,13 @@ class AppDomain extends Domain { _apps.remove(app); } }); + + await Future.any(>[ + appStartedCompleter.future.then((void value) { + _sendAppEvent(app, 'started'); + }), + appRunFuture, + ]); return app; } diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index a1bc3d40cd..9c8f64a11c 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -14,7 +14,6 @@ import '../base/file_system.dart'; import '../base/io.dart'; import '../base/utils.dart'; import '../build_info.dart'; -import '../daemon.dart'; import '../device.dart'; import '../features.dart'; import '../globals.dart' as globals; @@ -761,18 +760,7 @@ class RunCommand extends RunCommandBase { @visibleForTesting Daemon createMachineDaemon() { - final Daemon daemon = Daemon( - DaemonConnection( - daemonStreams: DaemonStreams.fromStdio(globals.stdio, logger: globals.logger), - logger: globals.logger, - ), - notifyingLogger: - (globals.logger is NotifyingLogger) - ? globals.logger as NotifyingLogger - : NotifyingLogger(verbose: globals.logger.isVerbose, parent: globals.logger), - logToStdout: true, - ); - return daemon; + return Daemon.createMachineDaemon(); } @override diff --git a/packages/flutter_tools/lib/src/commands/widget_preview.dart b/packages/flutter_tools/lib/src/commands/widget_preview.dart index ddc0dfebb6..862963d97e 100644 --- a/packages/flutter_tools/lib/src/commands/widget_preview.dart +++ b/packages/flutter_tools/lib/src/commands/widget_preview.dart @@ -9,31 +9,47 @@ import '../base/common.dart'; import '../base/deferred_component.dart'; import '../base/file_system.dart'; import '../base/logger.dart'; +import '../base/os.dart'; import '../base/platform.dart'; +import '../base/process.dart'; +import '../build_info.dart'; +import '../bundle.dart' as bundle; import '../cache.dart'; import '../convert.dart'; import '../dart/pub.dart'; +import '../device.dart'; import '../flutter_manifest.dart'; - +import '../linux/build_linux.dart'; +import '../macos/build_macos.dart'; import '../project.dart'; import '../runner/flutter_command.dart'; import '../widget_preview/preview_code_generator.dart'; import '../widget_preview/preview_detector.dart'; +import '../windows/build_windows.dart'; import 'create_base.dart'; +import 'daemon.dart'; class WidgetPreviewCommand extends FlutterCommand { WidgetPreviewCommand({ + required bool verboseHelp, required Logger logger, required FileSystem fs, required FlutterProjectFactory projectFactory, required Cache cache, + required Platform platform, + required ShutdownHooks shutdownHooks, + required OperatingSystemUtils os, }) { addSubcommand( WidgetPreviewStartCommand( + verboseHelp: verboseHelp, logger: logger, fs: fs, projectFactory: projectFactory, cache: cache, + platform: platform, + shutdownHooks: shutdownHooks, + os: os, ), ); addSubcommand( @@ -92,20 +108,36 @@ abstract base class WidgetPreviewSubCommandBase extends FlutterCommand { final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with CreateBase { WidgetPreviewStartCommand({ + this.verboseHelp = false, required this.logger, required this.fs, required this.projectFactory, required this.cache, + required this.platform, + required this.shutdownHooks, + required this.os, }) { addPubOptions(); + argParser.addFlag( + kLaunchPreviewer, + defaultsTo: true, + help: 'Launches the widget preview environment.', + // Should only be used for testing. + hide: !verboseHelp, + ); } + static const String kWidgetPreviewScaffoldName = 'widget_preview_scaffold'; + static const String kLaunchPreviewer = 'launch-previewer'; + @override String get description => 'Starts the widget preview environment.'; @override String get name => 'start'; + final bool verboseHelp; + @override final FileSystem fs; @@ -117,6 +149,12 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C final Cache cache; + final Platform platform; + + final ShutdownHooks shutdownHooks; + + final OperatingSystemUtils os; + late final PreviewDetector _previewDetector = PreviewDetector( logger: logger, fs: fs, @@ -125,6 +163,9 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C late final PreviewCodeGenerator _previewCodeGenerator; + /// The currently running instance of the widget preview scaffold. + AppInstance? _widgetPreviewApp; + @override Future runCommand() async { final FlutterProject rootProject = getRootProject(); @@ -132,44 +173,231 @@ final class WidgetPreviewStartCommand extends WidgetPreviewSubCommandBase with C // Check to see if a preview scaffold has already been generated. If not, // generate one. - if (!widgetPreviewScaffold.existsSync()) { + final bool generateScaffoldProject = !widgetPreviewScaffold.existsSync(); + widgetPreviewScaffold.createSync(); + + if (generateScaffoldProject) { + // WARNING: this log message is used by test/integration.shard/widget_preview_test.dart logger.printStatus('Creating widget preview scaffolding at: ${widgetPreviewScaffold.path}'); await generateApp( - ['widget_preview_scaffold'], + ['app', kWidgetPreviewScaffoldName], widgetPreviewScaffold, createTemplateContext( organization: 'flutter', - projectName: 'widget_preview_scaffold', + projectName: kWidgetPreviewScaffoldName, titleCaseProjectName: 'Widget Preview Scaffold', flutterRoot: Cache.flutterRoot!, dartSdkVersionBounds: '^${cache.dartSdkBuild}', - linux: const LocalPlatform().isLinux, - macos: const LocalPlatform().isMacOS, - windows: const LocalPlatform().isWindows, + linux: platform.isLinux, + macos: platform.isMacOS, + windows: platform.isWindows, ), overwrite: true, generateMetadata: false, ); - await _populatePreviewPubspec(rootProject: rootProject); + + // WARNING: this access of widgetPreviewScaffoldProject needs to happen + // after we generate the scaffold project as invoking the getter triggers + // lazy initialization of the preview scaffold's FlutterManifest before + // the scaffold project's pubspec has been generated. + // TODO(bkonyi): add logic to rebuild after SDK updates + await initialBuild(widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject); } - // WARNING: this needs to happen after we generate the scaffold project as invoking the - // widgetPreviewScaffoldProject getter triggers lazy initialization of the preview scaffold's - // FlutterManifest before the scaffold project's pubspec has been generated. _previewCodeGenerator = PreviewCodeGenerator( widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject, fs: fs, ); + // TODO(matanlurey): Remove this comment once flutter_gen is removed. + // + // Tracking removal: https://github.com/flutter/flutter/issues/102983. + // + // Populate the pubspec after the initial build to avoid blowing away the package_config.json + // which may have manual changes for flutter_gen support. + await _populatePreviewPubspec(rootProject: rootProject); + final PreviewMapping initialPreviews = await _previewDetector.initialize(rootProject.directory); _previewCodeGenerator.populatePreviewsInGeneratedPreviewScaffold(initialPreviews); + if (boolArg(kLaunchPreviewer)) { + shutdownHooks.addShutdownHook(() async { + await _widgetPreviewApp?.stop(); + }); + _widgetPreviewApp = await runPreviewEnvironment( + widgetPreviewScaffoldProject: rootProject.widgetPreviewScaffoldProject, + ); + final int result = await _widgetPreviewApp!.runner.waitForAppToFinish(); + if (result != 0) { + throwToolExit('Failed to launch the widget previewer.', exitCode: result); + } + } + await _previewDetector.dispose(); return FlutterCommandResult.success(); } void onChangeDetected(PreviewMapping previews) { - // TODO(bkonyi): perform hot reload + logger.printStatus('Triggering reload based on change to preview set: $previews'); + _widgetPreviewApp?.restart(); + } + + /// Builds the application binary for the widget preview scaffold the first + /// time the widget preview command is run. + /// + /// The resulting binary is used to speed up subsequent widget previewer launches + /// by acting as a basic scaffold to load previews into using hot reload / restart. + Future initialBuild({required FlutterProject widgetPreviewScaffoldProject}) async { + // TODO(bkonyi): handle error case where desktop device isn't enabled. + await widgetPreviewScaffoldProject.ensureReadyForPlatformSpecificTooling( + linuxPlatform: platform.isLinux, + macOSPlatform: platform.isMacOS, + windowsPlatform: platform.isWindows, + allowedPlugins: const [], + ); + + // Generate initial package_config.json, otherwise the build will fail. + await pub.get( + context: PubContext.create, + project: widgetPreviewScaffoldProject, + offline: offline, + outputMode: PubOutputMode.summaryOnly, + ); + + // WARNING: this log message is used by test/integration.shard/widget_preview_test.dart + logger.printStatus('Performing initial build of the Widget Preview Scaffold...'); + + final BuildInfo buildInfo = BuildInfo( + BuildMode.debug, + null, + treeShakeIcons: false, + packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path, + ); + + if (platform.isMacOS) { + await buildMacOS( + flutterProject: widgetPreviewScaffoldProject, + buildInfo: buildInfo, + verboseLogging: false, + ); + } else if (platform.isLinux) { + await buildLinux( + widgetPreviewScaffoldProject.linux, + buildInfo, + targetPlatform: + os.hostPlatform == HostPlatform.linux_x64 + ? TargetPlatform.linux_x64 + : TargetPlatform.linux_arm64, + logger: logger, + ); + } else if (platform.isWindows) { + await buildWindows( + widgetPreviewScaffoldProject.windows, + buildInfo, + os.hostPlatform == HostPlatform.windows_x64 + ? TargetPlatform.windows_x64 + : TargetPlatform.windows_arm64, + ); + } else { + throw UnimplementedError(); + } + // WARNING: this log message is used by test/integration.shard/widget_preview_test.dart + logger.printStatus('Widget Preview Scaffold initial build complete.'); + } + + /// Returns the path to a prebuilt widget_preview_scaffold application binary. + String prebuiltApplicationBinaryPath({required FlutterProject widgetPreviewScaffoldProject}) { + assert(platform.isLinux || platform.isMacOS || platform.isWindows); + String path; + if (platform.isMacOS) { + path = fs.path.join( + getMacOSBuildDirectory(), + 'Build/Products/Debug/widget_preview_scaffold.app', + ); + } else if (platform.isLinux) { + path = fs.path.join( + getLinuxBuildDirectory( + os.hostPlatform == HostPlatform.linux_x64 + ? TargetPlatform.linux_x64 + : TargetPlatform.linux_arm64, + ), + 'debug/bundle/widget_preview_scaffold', + ); + } else if (platform.isWindows) { + path = fs.path.join( + getWindowsBuildDirectory( + os.hostPlatform == HostPlatform.windows_x64 + ? TargetPlatform.windows_x64 + : TargetPlatform.windows_arm64, + ), + 'runner/Debug/widget_preview_scaffold.exe', + ); + } else { + throw StateError('Unknown OS'); + } + path = fs.path.join(widgetPreviewScaffoldProject.directory.path, path); + if (fs.typeSync(path) == FileSystemEntityType.notFound) { + logger.printStatus(fs.currentDirectory.toString()); + throw StateError('Could not find prebuilt application binary at $path.'); + } + return path; + } + + Future runPreviewEnvironment({ + required FlutterProject widgetPreviewScaffoldProject, + }) async { + final AppInstance app; + try { + // Since the only target supported by the widget preview scaffold is the host's desktop + // device, only a single desktop device should be returned. + final List devices = await deviceManager!.getDevices( + filter: DeviceDiscoveryFilter( + supportFilter: DeviceDiscoverySupportFilter.excludeDevicesUnsupportedByFlutterOrProject( + flutterProject: widgetPreviewScaffoldProject, + ), + deviceConnectionInterface: DeviceConnectionInterface.attached, + ), + ); + assert(devices.length == 1); + final Device device = devices.first; + + // We launch from a prebuilt widget preview scaffold instance to reduce launch times after + // the first run. + final File prebuiltApplicationBinary = fs.file( + prebuiltApplicationBinaryPath(widgetPreviewScaffoldProject: widgetPreviewScaffoldProject), + ); + const String? kEmptyRoute = null; + const bool kEnableHotReload = true; + + app = await Daemon.createMachineDaemon().appDomain.startApp( + device, + widgetPreviewScaffoldProject.directory.path, + bundle.defaultMainPath, + kEmptyRoute, // route + DebuggingOptions.enabled( + BuildInfo( + BuildMode.debug, + null, + treeShakeIcons: false, + packageConfigPath: widgetPreviewScaffoldProject.packageConfig.path, + ), + ), + kEnableHotReload, // hot mode + applicationBinary: prebuiltApplicationBinary, + trackWidgetCreation: false, + projectRootPath: widgetPreviewScaffoldProject.directory.path, + ); + } on Exception catch (error) { + throwToolExit(error.toString()); + } + // Immediately perform a hot restart to ensure new previews are loaded into the prebuilt + // application. + // WARNING: this log message is used by test/integration.shard/widget_preview_test.dart + logger.printStatus('Loading previews into the Widget Preview Scaffold...'); + await app.restart(fullRestart: true); + // WARNING: this log message is used by test/integration.shard/widget_preview_test.dart + logger.printStatus('Done loading previews.'); + return app; } @visibleForTesting diff --git a/packages/flutter_tools/lib/src/linux/build_linux.dart b/packages/flutter_tools/lib/src/linux/build_linux.dart index 71d89f004f..936f31d101 100644 --- a/packages/flutter_tools/lib/src/linux/build_linux.dart +++ b/packages/flutter_tools/lib/src/linux/build_linux.dart @@ -80,9 +80,9 @@ Future buildLinux( final Status status = logger.startProgress('Building Linux application...'); final String buildModeName = buildInfo.mode.cliName; - final Directory platformBuildDirectory = globals.fs.directory( - getLinuxBuildDirectory(targetPlatform), - ); + final Directory platformBuildDirectory = globals.fs + .directory(linuxProject.parent.directory.path) + .childDirectory(getLinuxBuildDirectory(targetPlatform)); final Directory buildDirectory = platformBuildDirectory.childDirectory(buildModeName); try { await _runCmake( diff --git a/packages/flutter_tools/lib/src/macos/application_package.dart b/packages/flutter_tools/lib/src/macos/application_package.dart index c0e3ae104b..1359d781ed 100644 --- a/packages/flutter_tools/lib/src/macos/application_package.dart +++ b/packages/flutter_tools/lib/src/macos/application_package.dart @@ -178,6 +178,7 @@ class BuildableMacOSApp extends MacOSApp { } return globals.fs.path.join( + project.parent.directory.path, getMacOSBuildDirectory(), 'Build', 'Products', diff --git a/packages/flutter_tools/lib/src/macos/build_macos.dart b/packages/flutter_tools/lib/src/macos/build_macos.dart index 6413faead7..1ca674df59 100644 --- a/packages/flutter_tools/lib/src/macos/build_macos.dart +++ b/packages/flutter_tools/lib/src/macos/build_macos.dart @@ -111,7 +111,9 @@ Future buildMacOS({ final ProjectMigration migration = ProjectMigration(migrators); await migration.run(); - final Directory flutterBuildDir = globals.fs.directory(getMacOSBuildDirectory()); + final Directory flutterBuildDir = flutterProject.directory.childDirectory( + getMacOSBuildDirectory(), + ); if (!flutterBuildDir.existsSync()) { flutterBuildDir.createSync(recursive: true); } diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart b/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart index 18486394ea..83d7e3c66c 100644 --- a/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart +++ b/packages/flutter_tools/lib/src/widget_preview/preview_code_generator.dart @@ -52,13 +52,13 @@ class PreviewCodeGenerator { b ..body = literalList([ - for (final MapEntry>( - key: String path, + for (final MapEntry>( + key: (path: String _, :Uri uri), value: List previewMethods, ) in previews.entries) ...[ for (final String method in previewMethods) - refer(method, path).call([]), + refer(method, uri.toString()).call([]), ], ]).code ..name = 'previews' diff --git a/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart b/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart index 0ca345e1e2..abb463b9e4 100644 --- a/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart +++ b/packages/flutter_tools/lib/src/widget_preview/preview_detector.dart @@ -18,7 +18,15 @@ import '../base/logger.dart'; import '../base/utils.dart'; import 'preview_code_generator.dart'; -typedef PreviewMapping = Map>; +/// A path / URI pair used to map previews to a file. +/// +/// We don't just use a path or a URI as the file watcher doesn't report URIs +/// (e.g., package:*) but the analyzer APIs do, and the code generator emits +/// package URIs for preview imports. +typedef PreviewPath = ({String path, Uri uri}); + +/// Represents a set of previews for a given file. +typedef PreviewMapping = Map>; class PreviewDetector { PreviewDetector({required this.fs, required this.logger, required this.onChangeDetected}); @@ -38,7 +46,7 @@ class PreviewDetector { final Watcher watcher = Watcher(projectRoot.path); // TODO(bkonyi): watch for changes to pubspec.yaml _fileWatcher = watcher.events.listen((WatchEvent event) async { - final String eventPath = Uri.file(event.path).toString(); + final String eventPath = event.path; // Only trigger a reload when changes to Dart sources are detected. We // ignore the generated preview file to avoid getting stuck in a loop. if (!eventPath.endsWith('.dart') || @@ -49,7 +57,9 @@ class PreviewDetector { final PreviewMapping filePreviewsMapping = findPreviewFunctions( fs.file(Uri.file(event.path)), ); - if (filePreviewsMapping.isEmpty && !_pathToPreviews.containsKey(eventPath)) { + final bool hasExistingPreviews = + _pathToPreviews.keys.where((PreviewPath e) => e.path == event.path).isNotEmpty; + if (filePreviewsMapping.isEmpty && !hasExistingPreviews) { // No previews found or removed, nothing to do. return; } @@ -59,20 +69,21 @@ class PreviewDetector { } if (filePreviewsMapping.isNotEmpty) { // The set of previews has changed, but there are still previews in the file. - final MapEntry>(key: String uri, value: List filePreviews) = - filePreviewsMapping.entries.first; - assert(uri == eventPath); - logger.printStatus('Updated previews for $eventPath: $filePreviews'); + final MapEntry>( + key: PreviewPath location, + value: List filePreviews, + ) = filePreviewsMapping.entries.first; + logger.printStatus('Updated previews for ${location.uri}: $filePreviews'); if (filePreviews.isNotEmpty) { - final List? currentPreviewsForFile = _pathToPreviews[eventPath]; + final List? currentPreviewsForFile = _pathToPreviews[location]; if (filePreviews != currentPreviewsForFile) { - _pathToPreviews[eventPath] = filePreviews; + _pathToPreviews[location] = filePreviews; } } } else { // The file previously had previews that were removed. logger.printStatus('Previews removed from $eventPath'); - _pathToPreviews.remove(eventPath); + _pathToPreviews.removeWhere((PreviewPath e, _) => e.path == eventPath); } onChangeDetected(_pathToPreviews); }); @@ -106,7 +117,8 @@ class PreviewDetector { final SomeParsedLibraryResult lib = context.currentSession.getParsedLibrary(filePath); if (lib is ParsedLibraryResult) { for (final ParsedUnitResult unit in lib.units) { - final List previewEntries = previews[unit.uri.toString()] ?? []; + final List previewEntries = + previews[(path: unit.path, uri: unit.uri)] ?? []; for (final SyntacticEntity entity in unit.unit.childEntities) { if (entity is FunctionDeclaration && !entity.name.toString().startsWith('_')) { bool foundPreview = false; @@ -127,7 +139,7 @@ class PreviewDetector { } } if (previewEntries.isNotEmpty) { - previews[unit.uri.toString()] = previewEntries; + previews[(path: unit.path, uri: unit.uri)] = previewEntries; } } } else { diff --git a/packages/flutter_tools/templates/widget_preview_scaffold/lib/main.dart.tmpl b/packages/flutter_tools/templates/widget_preview_scaffold/lib/main.dart.tmpl index 3cba337835..8a0b5e7c36 100644 --- a/packages/flutter_tools/templates/widget_preview_scaffold/lib/main.dart.tmpl +++ b/packages/flutter_tools/templates/widget_preview_scaffold/lib/main.dart.tmpl @@ -2,6 +2,25 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -Future main() async { - // TODO(bkonyi): implement. +// TODO(bkonyi): Implement. + +import 'package:flutter/material.dart'; + +void main() { + runApp(const MainApp()); +} + +class MainApp extends StatelessWidget { + const MainApp({super.key}); + + @override + Widget build(BuildContext context) { + return const MaterialApp( + home: Scaffold( + body: Center( + child: Text('Hello World!'), + ), + ), + ); + } } diff --git a/packages/flutter_tools/test/commands.shard/hermetic/build_linux_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/build_linux_test.dart index 2cdced95ce..bb7ffd9299 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/build_linux_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/build_linux_test.dart @@ -91,7 +91,7 @@ void main() { '-DFLUTTER_TARGET_PLATFORM=linux-$target', '/linux', ], - workingDirectory: 'build/linux/$target/$buildMode', + workingDirectory: '/build/linux/$target/$buildMode', onRun: onRun, ); } @@ -105,7 +105,7 @@ void main() { String stdout = '', }) { return FakeCommand( - command: ['ninja', '-C', 'build/linux/$target/$buildMode', 'install'], + command: ['ninja', '-C', '/build/linux/$target/$buildMode', 'install'], environment: environment, onRun: onRun, stdout: stdout, @@ -396,7 +396,7 @@ void main() { // This contains a mix of routine build output and various types of errors // (Dart error, compile error, link error), edited down for compactness. const String stdout = r''' -ninja: Entering directory `build/linux/x64/release' +ninja: Entering directory `/build/linux/x64/release' [1/6] Generating /foo/linux/flutter/ephemeral/libflutter_linux_gtk.so, /foo/linux/flutter/ephemeral/flutter_linux/flutter_linux.h, _phony lib/main.dart:4:3: Error: Method not found: 'foo'. [2/6] Building CXX object CMakeFiles/foo.dir/main.cc.o diff --git a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview_test.dart index db8a316a5e..4c8c57b163 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/widget_preview_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/widget_preview_test.dart @@ -5,6 +5,9 @@ import 'package:file/memory.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/os.dart'; +import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/widget_preview.dart'; import 'package:flutter_tools/src/convert.dart'; @@ -24,6 +27,7 @@ void main() { late WidgetPreviewStartCommand command; late FlutterProject rootProject; late Logger logger; + final Platform platform = FakePlatform.fromPlatform(const LocalPlatform()); setUp(() { fileSystem = MemoryFileSystem.test(); @@ -33,7 +37,15 @@ void main() { fs: fileSystem, projectFactory: FakeFlutterProjectFactory(), logger: logger, - cache: Cache.test(processManager: processManager), + cache: Cache.test(processManager: processManager, platform: platform), + platform: platform, + shutdownHooks: ShutdownHooks(), + os: OperatingSystemUtils( + fileSystem: fileSystem, + logger: logger, + platform: platform, + processManager: processManager, + ), ); rootProject = FakeFlutterProject( projectRoot: 'some_project', diff --git a/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart b/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart index 711490b1fd..0426c2a410 100644 --- a/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart +++ b/packages/flutter_tools/test/commands.shard/permeable/widget_preview_test.dart @@ -10,7 +10,9 @@ import 'package:flutter_tools/src/base/bot_detector.dart'; import 'package:flutter_tools/src/base/common.dart'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/os.dart'; import 'package:flutter_tools/src/base/platform.dart'; +import 'package:flutter_tools/src/base/process.dart'; import 'package:flutter_tools/src/base/signals.dart'; import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/commands/widget_preview.dart'; @@ -40,7 +42,7 @@ void main() { botDetector = const FakeBotDetector(false); tempDir = fs.systemTempDirectory.createTempSync('flutter_tools_create_test.'); mockStdio = FakeStdio(); - platform = const LocalPlatform(); + platform = FakePlatform.fromPlatform(const LocalPlatform()); }); tearDown(() { @@ -58,10 +60,19 @@ void main() { Future runWidgetPreviewCommand(List arguments) async { final CommandRunner runner = createTestCommandRunner( WidgetPreviewCommand( + verboseHelp: false, logger: logger, fs: fs, projectFactory: FlutterProjectFactory(logger: logger, fileSystem: fs), cache: Cache.test(processManager: loggingProcessManager, platform: platform), + platform: platform, + shutdownHooks: ShutdownHooks(), + os: OperatingSystemUtils( + fileSystem: fs, + processManager: loggingProcessManager, + logger: logger, + platform: platform, + ), ), ); await runner.run(['widget-preview', ...arguments]); @@ -74,6 +85,7 @@ void main() { await runWidgetPreviewCommand([ 'start', ...?arguments, + '--no-launch-previewer', if (rootProject != null) rootProject.path, ]); final Directory widgetPreviewScaffoldDir = widgetPreviewScaffoldFromRootProject( @@ -245,7 +257,7 @@ import 'package:flutter_project/foo.dart' as _i1;import 'package:widget_preview/ ); testUsingContext( - 'clean deletes .dart_tool/widget_preview_scaffold', + 'start finds existing previews in the CWD and injects them into ${PreviewCodeGenerator.generatedPreviewFilePath}', () async { final Directory rootProject = await createRootProject(); await startWidgetPreview(rootProject: rootProject); diff --git a/packages/flutter_tools/test/general.shard/widget_preview/preview_code_generator_test.dart b/packages/flutter_tools/test/general.shard/widget_preview/preview_code_generator_test.dart index e5e12edd97..cdc2529771 100644 --- a/packages/flutter_tools/test/general.shard/widget_preview/preview_code_generator_test.dart +++ b/packages/flutter_tools/test/general.shard/widget_preview/preview_code_generator_test.dart @@ -9,6 +9,7 @@ import 'package:flutter_tools/src/base/logger.dart'; import 'package:flutter_tools/src/flutter_manifest.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/widget_preview/preview_code_generator.dart'; +import 'package:flutter_tools/src/widget_preview/preview_detector.dart'; import 'package:test/test.dart'; import '../../src/context.dart'; @@ -39,9 +40,9 @@ void main() { expect(generatedPreviewFile, isNot(exists)); // Populate the generated preview file. - codeGenerator.populatePreviewsInGeneratedPreviewScaffold(const >{ - 'foo.dart': ['preview'], - 'src/bar.dart': ['barPreview1', 'barPreview2'], + codeGenerator.populatePreviewsInGeneratedPreviewScaffold(>{ + (path: '', uri: Uri(path: 'foo.dart')): ['preview'], + (path: '', uri: Uri(path: 'src/bar.dart')): ['barPreview1', 'barPreview2'], }); expect(generatedPreviewFile, exists); @@ -59,7 +60,9 @@ import 'foo.dart' as _i1;import 'src/bar.dart' as _i2;import 'package:widget_pre expect(generatedPreviewFile.readAsStringSync(), expectedGeneratedPreviewFileContents); // Regenerate the generated file with no previews. - codeGenerator.populatePreviewsInGeneratedPreviewScaffold(const >{}); + codeGenerator.populatePreviewsInGeneratedPreviewScaffold( + const >{}, + ); expect(generatedPreviewFile, exists); // The generated file should only contain: diff --git a/packages/flutter_tools/test/general.shard/widget_preview/preview_detector_test.dart b/packages/flutter_tools/test/general.shard/widget_preview/preview_detector_test.dart index 6c55b64580..5e249c1ceb 100644 --- a/packages/flutter_tools/test/general.shard/widget_preview/preview_detector_test.dart +++ b/packages/flutter_tools/test/general.shard/widget_preview/preview_detector_test.dart @@ -6,6 +6,7 @@ import 'dart:async'; import 'package:flutter_tools/src/base/file_system.dart'; import 'package:flutter_tools/src/base/logger.dart'; +import 'package:flutter_tools/src/base/platform.dart'; import 'package:flutter_tools/src/base/signals.dart'; import 'package:flutter_tools/src/widget_preview/preview_detector.dart'; import 'package:test/test.dart'; @@ -17,16 +18,20 @@ Directory createBasicProjectStructure(FileSystem fs) { return fs.systemTempDirectory.createTempSync('root'); } -File addPreviewContainingFile(Directory projectRoot, String path) { - return projectRoot.childDirectory('lib').childFile(path) - ..createSync(recursive: true) - ..writeAsStringSync(previewContainingFileContents); +PreviewPath addPreviewContainingFile(Directory projectRoot, List path) { + final File file = + projectRoot.childDirectory('lib').childFile(path.join(const LocalPlatform().pathSeparator)) + ..createSync(recursive: true) + ..writeAsStringSync(previewContainingFileContents); + return (path: file.path, uri: file.uri); } -File addNonPreviewContainingFile(Directory projectRoot, String path) { - return projectRoot.childDirectory('lib').childFile(path) - ..createSync(recursive: true) - ..writeAsStringSync(nonPreviewContainingFileContents); +PreviewPath addNonPreviewContainingFile(Directory projectRoot, List path) { + final File file = + projectRoot.childDirectory('lib').childFile(path.join(const LocalPlatform().pathSeparator)) + ..createSync(recursive: true) + ..writeAsStringSync(nonPreviewContainingFileContents); + return (path: file.path, uri: file.uri); } void main() { @@ -62,29 +67,32 @@ void main() { }); testUsingContext('can detect previews in existing files', () async { - final List previewFiles = [ - addPreviewContainingFile(projectRoot, 'foo.dart'), - addPreviewContainingFile(projectRoot, 'src/bar.dart'), + final List previewFiles = [ + addPreviewContainingFile(projectRoot, ['foo.dart']), + addPreviewContainingFile(projectRoot, ['src', 'bar.dart']), ]; - addNonPreviewContainingFile(projectRoot, 'baz.dart'); + addNonPreviewContainingFile(projectRoot, ['baz.dart']); final PreviewMapping mapping = previewDetector.findPreviewFunctions(projectRoot); - expect(mapping.keys.toSet(), previewFiles.map((File e) => e.uri.toString()).toSet()); + expect(mapping.keys.toSet(), previewFiles.toSet()); }); testUsingContext('can detect previews in updated files', () async { // Create two files with existing previews and one without. - final PreviewMapping expectedInitialMapping = >{ - addPreviewContainingFile(projectRoot, 'foo.dart').uri.toString(): ['previews'], - addPreviewContainingFile(projectRoot, 'src/bar.dart').uri.toString(): ['previews'], + final PreviewMapping expectedInitialMapping = >{ + addPreviewContainingFile(projectRoot, ['foo.dart']): ['previews'], + addPreviewContainingFile(projectRoot, ['src', 'bar.dart']): ['previews'], }; - final File nonPreviewContainingFile = addNonPreviewContainingFile(projectRoot, 'baz.dart'); + final PreviewPath nonPreviewContainingFile = addNonPreviewContainingFile( + projectRoot, + ['baz.dart'], + ); Completer completer = Completer(); onChangeDetected = (PreviewMapping updated) { // The new preview in baz.dart should be included in the preview mapping. - expect(updated, >{ + expect(updated, >{ ...expectedInitialMapping, - nonPreviewContainingFile.uri.toString(): ['previews'], + nonPreviewContainingFile: ['previews'], }); completer.complete(); }; @@ -94,7 +102,7 @@ void main() { // Update the file without an existing preview to include a preview and ensure it triggers // the preview detector. - addPreviewContainingFile(projectRoot, 'baz.dart'); + addPreviewContainingFile(projectRoot, ['baz.dart']); await completer.future; completer = Completer(); @@ -106,7 +114,7 @@ void main() { // Update the file with an existing preview to remove the preview and ensure it triggers // the preview detector. - addNonPreviewContainingFile(projectRoot, 'baz.dart'); + addNonPreviewContainingFile(projectRoot, ['baz.dart']); await completer.future; }); }); diff --git a/packages/flutter_tools/test/integration.shard/widget_preview_test.dart b/packages/flutter_tools/test/integration.shard/widget_preview_test.dart new file mode 100644 index 0000000000..6ebe0ea6e3 --- /dev/null +++ b/packages/flutter_tools/test/integration.shard/widget_preview_test.dart @@ -0,0 +1,92 @@ +// 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 'dart:convert'; + +import 'package:file/file.dart'; +import 'package:flutter_tools/src/base/io.dart'; +import 'package:process/process.dart'; + +import '../src/common.dart'; +import 'test_data/basic_project.dart'; +import 'test_utils.dart'; + +const List firstLaunchMessages = [ + 'Creating widget preview scaffolding at:', + 'Performing initial build of the Widget Preview Scaffold...', + 'Widget Preview Scaffold initial build complete.', + 'Loading previews into the Widget Preview Scaffold...', + 'Done loading previews.', +]; + +const List subsequentLaunchMessages = [ + 'Loading previews into the Widget Preview Scaffold...', + 'Done loading previews.', +]; + +void main() { + late Directory tempDir; + Process? process; + final BasicProject project = BasicProject(); + const ProcessManager processManager = LocalProcessManager(); + + setUp(() async { + tempDir = createResolvedTempDirectorySync('widget_preview_test.'); + await project.setUpIn(tempDir); + }); + + tearDown(() async { + process?.kill(); + process = null; + tryToDelete(tempDir); + }); + + Future runWidgetPreview({required List expectedMessages}) async { + expect(expectedMessages, isNotEmpty); + int i = 0; + process = await processManager.start([ + flutterBin, + 'widget-preview', + 'start', + ], workingDirectory: tempDir.path); + + final Completer completer = Completer(); + process!.stdout.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) { + printOnFailure('STDOUT: $msg'); + if (completer.isCompleted) { + return; + } + if (msg.contains(expectedMessages[i])) { + ++i; + } + if (i == expectedMessages.length) { + completer.complete(); + } + }); + + process!.stderr.transform(utf8.decoder).transform(const LineSplitter()).listen((String msg) { + printOnFailure('STDERR: $msg'); + }); + + await completer.future; + process!.kill(); + process = null; + } + + group('flutter widget-preview start', () { + testWithoutContext('smoke test', () async { + await runWidgetPreview(expectedMessages: firstLaunchMessages); + }); + + testWithoutContext('does not rebuild project on subsequent runs', () async { + // The first run of 'flutter widget-preview start' should generate a new preview scaffold and + // pre-build the application. + await runWidgetPreview(expectedMessages: firstLaunchMessages); + + // We shouldn't regenerate the scaffold after the initial run. + await runWidgetPreview(expectedMessages: subsequentLaunchMessages); + }); + }); +}