From 53c2f7086be319487eb07fa9aa2ad33ac59170af Mon Sep 17 00:00:00 2001 From: Chris Bracken Date: Wed, 12 May 2021 20:45:30 -0700 Subject: [PATCH] [flutter_tools] support flutter run -d winuwp (#82373) Allow flutter run to work end-to-end with a UWP device. Uses win32/ffi for the actual launch of the application, injected via the native API class. This is structured to avoid a g3 dependency. Install and amuid require powershell scripts for now. Actually connecting to the observatory requires running a command in an elevated prompt. Instructions are presented to the user if a terminal is attached. This is a rebased version of https://github.com/flutter/flutter/pull/79684 by @jonahwilliams, updated to remove `NativeApi` and replace is with calls to `uwptool`. Part of https://github.com/flutter/flutter/issues/82085 --- .../flutter_tools/bin/getaumidfromname.ps1 | 13 ++ packages/flutter_tools/lib/src/artifacts.dart | 37 +++- packages/flutter_tools/lib/src/cmake.dart | 18 ++ .../flutter_tools/lib/src/context_runner.dart | 6 + .../lib/src/flutter_application_package.dart | 4 +- .../lib/src/flutter_device_manager.dart | 3 + packages/flutter_tools/lib/src/project.dart | 31 +++ .../lib/src/runner/flutter_command.dart | 4 +- .../lib/src/windows/application_package.dart | 11 + .../lib/src/windows/uwptool.dart | 87 ++++++++ .../lib/src/windows/windows_device.dart | 191 +++++++++++++++--- .../general.shard/windows/project_test.dart | 140 +++++++++++++ .../windows/windows_device_test.dart | 131 +++++++++++- 13 files changed, 642 insertions(+), 34 deletions(-) create mode 100644 packages/flutter_tools/bin/getaumidfromname.ps1 create mode 100644 packages/flutter_tools/lib/src/windows/uwptool.dart create mode 100644 packages/flutter_tools/test/general.shard/windows/project_test.dart diff --git a/packages/flutter_tools/bin/getaumidfromname.ps1 b/packages/flutter_tools/bin/getaumidfromname.ps1 new file mode 100644 index 00000000000..d5873e33e81 --- /dev/null +++ b/packages/flutter_tools/bin/getaumidfromname.ps1 @@ -0,0 +1,13 @@ +# 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. + +# Retrieves the AMUID from a given application name +[CmdletBinding()] +param( + [Parameter()] + [string]$Name +) +$foo = get-appxpackage | Where-Object { $_.Name -like $name } +$aumid = $foo.packagefamilyname + "!" + (Get-AppxPackageManifest $foo).package.applications.application.id +Write-Output $aumid diff --git a/packages/flutter_tools/lib/src/artifacts.dart b/packages/flutter_tools/lib/src/artifacts.dart index f5d404daee9..a5e7c8dddca 100644 --- a/packages/flutter_tools/lib/src/artifacts.dart +++ b/packages/flutter_tools/lib/src/artifacts.dart @@ -55,6 +55,9 @@ enum Artifact { /// Tools related to subsetting or icon font files. fontSubset, constFinder, + + // Windows UWP app management tool. + uwptool, } /// A subset of [Artifact]s that are platform and build mode independent @@ -111,8 +114,31 @@ String _enginePlatformDirectoryName(TargetPlatform platform) { return getNameForTargetPlatform(platform); } +bool _isWindows(TargetPlatform platform) { + switch (platform) { + case TargetPlatform.windows_x64: + case TargetPlatform.windows_uwp_x64: + return true; + case TargetPlatform.android: + case TargetPlatform.android_arm: + case TargetPlatform.android_arm64: + case TargetPlatform.android_x64: + case TargetPlatform.android_x86: + case TargetPlatform.darwin: + case TargetPlatform.fuchsia_arm64: + case TargetPlatform.fuchsia_x64: + case TargetPlatform.ios: + case TargetPlatform.linux_arm64: + case TargetPlatform.linux_x64: + case TargetPlatform.tester: + case TargetPlatform.web_javascript: + return false; + } + return false; +} + String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMode mode ]) { - final String exe = platform == TargetPlatform.windows_x64 ? '.exe' : ''; + final String exe = _isWindows(platform) ? '.exe' : ''; switch (artifact) { case Artifact.genSnapshot: return 'gen_snapshot'; @@ -162,6 +188,8 @@ String _artifactToFileName(Artifact artifact, [ TargetPlatform platform, BuildMo return 'font-subset$exe'; case Artifact.constFinder: return 'const_finder.dart.snapshot'; + case Artifact.uwptool: + return 'uwptool$exe'; } assert(false, 'Invalid artifact $artifact.'); return null; @@ -543,6 +571,11 @@ class CachedArtifacts implements Artifacts { .childDirectory(_enginePlatformDirectoryName(platform)) .childFile(_artifactToFileName(artifact, platform, mode)) .path; + case Artifact.uwptool: + return _cache.getArtifactDirectory('engine') + .childDirectory('windows-uwp-x64-${getNameForBuildMode(mode ?? BuildMode.debug)}') + .childFile(_artifactToFileName(artifact, platform, mode)) + .path; default: assert(false, 'Artifact $artifact not available for platform $platform.'); return null; @@ -824,6 +857,8 @@ class CachedLocalEngineArtifacts implements LocalEngineArtifacts { case Artifact.frontendServerSnapshotForEngineDartSdk: return _fileSystem.path.join(_hostEngineOutPath, 'gen', artifactFileName); break; + case Artifact.uwptool: + return _fileSystem.path.join(_hostEngineOutPath, artifactFileName); } assert(false, 'Invalid artifact $artifact.'); return null; diff --git a/packages/flutter_tools/lib/src/cmake.dart b/packages/flutter_tools/lib/src/cmake.dart index 7dd735d9c01..9ad626e52f9 100644 --- a/packages/flutter_tools/lib/src/cmake.dart +++ b/packages/flutter_tools/lib/src/cmake.dart @@ -4,6 +4,7 @@ // @dart = 2.8 +import 'base/file_system.dart'; import 'project.dart'; /// Extracts the `BINARY_NAME` from a project's CMake file. @@ -23,6 +24,23 @@ String getCmakeExecutableName(CmakeBasedProject project) { return null; } +/// Extracts the `PACKAGE_GUID` from a project's CMake file. +/// +/// Returns `null` if it cannot be found. +String getCmakePackageGuid(File cmakeFile) { + if (!cmakeFile.existsSync()) { + return null; + } + final RegExp nameSetPattern = RegExp(r'^\s*set\(PACKAGE_GUID\s*"(.*)"\s*\)\s*$'); + for (final String line in cmakeFile.readAsLinesSync()) { + final RegExpMatch match = nameSetPattern.firstMatch(line); + if (match != null) { + return match.group(1); + } + } + return null; +} + String _escapeBackslashes(String s) { return s.replaceAll(r'\', r'\\'); } diff --git a/packages/flutter_tools/lib/src/context_runner.dart b/packages/flutter_tools/lib/src/context_runner.dart index 4c38bfc5e31..8769d1f969a 100644 --- a/packages/flutter_tools/lib/src/context_runner.dart +++ b/packages/flutter_tools/lib/src/context_runner.dart @@ -64,6 +64,7 @@ import 'run_hot.dart'; import 'runner/local_engine.dart'; import 'version.dart'; import 'web/workflow.dart'; +import 'windows/uwptool.dart'; import 'windows/visual_studio.dart'; import 'windows/visual_studio_validator.dart'; import 'windows/windows_workflow.dart'; @@ -206,6 +207,11 @@ Future runInContext( logger: globals.logger, platform: globals.platform ), + uwptool: UwpTool( + artifacts: globals.artifacts, + logger: globals.logger, + processManager: globals.processManager, + ), ), DevtoolsLauncher: () => DevtoolsServerLauncher( processManager: globals.processManager, diff --git a/packages/flutter_tools/lib/src/flutter_application_package.dart b/packages/flutter_tools/lib/src/flutter_application_package.dart index 65beb69bfb3..11b3f927cd5 100644 --- a/packages/flutter_tools/lib/src/flutter_application_package.dart +++ b/packages/flutter_tools/lib/src/flutter_application_package.dart @@ -108,9 +108,7 @@ class FlutterApplicationPackageFactory extends ApplicationPackageFactory { ? FuchsiaApp.fromFuchsiaProject(FlutterProject.current().fuchsia) : FuchsiaApp.fromPrebuiltApp(applicationBinary); case TargetPlatform.windows_uwp_x64: - return applicationBinary == null - ? WindowsApp.fromWindowsProject(FlutterProject.current().windowsUwp) - : WindowsApp.fromPrebuiltApp(applicationBinary); + return BuildableUwpApp(project: FlutterProject.current().windowsUwp); } assert(platform != null); return null; diff --git a/packages/flutter_tools/lib/src/flutter_device_manager.dart b/packages/flutter_tools/lib/src/flutter_device_manager.dart index 08b005be1b5..10c3158c8bf 100644 --- a/packages/flutter_tools/lib/src/flutter_device_manager.dart +++ b/packages/flutter_tools/lib/src/flutter_device_manager.dart @@ -35,6 +35,7 @@ import 'macos/xcdevice.dart'; import 'tester/flutter_tester.dart'; import 'version.dart'; import 'web/web_device.dart'; +import 'windows/uwptool.dart'; import 'windows/windows_device.dart'; import 'windows/windows_workflow.dart'; @@ -61,6 +62,7 @@ class FlutterDeviceManager extends DeviceManager { @required WindowsWorkflow windowsWorkflow, @required Terminal terminal, @required CustomDevicesConfig customDevicesConfig, + @required UwpTool uwptool, }) : deviceDiscoverers = [ AndroidDevices( logger: logger, @@ -118,6 +120,7 @@ class FlutterDeviceManager extends DeviceManager { fileSystem: fileSystem, windowsWorkflow: windowsWorkflow, featureFlags: featureFlags, + uwptool: uwptool, ), WebDevices( featureFlags: featureFlags, diff --git a/packages/flutter_tools/lib/src/project.dart b/packages/flutter_tools/lib/src/project.dart index 0dea5ca97da..d72f5d7cc06 100644 --- a/packages/flutter_tools/lib/src/project.dart +++ b/packages/flutter_tools/lib/src/project.dart @@ -16,6 +16,7 @@ import 'base/file_system.dart'; import 'base/logger.dart'; import 'build_info.dart'; import 'bundle.dart' as bundle; +import 'cmake.dart'; import 'features.dart'; import 'flutter_manifest.dart'; import 'flutter_plugins.dart'; @@ -1221,8 +1222,38 @@ class WindowsUwpProject extends WindowsProject { @override String get _childDirectory => 'winuwp'; + File get runnerCmakeFile => _editableDirectory.childDirectory('runner_uwp').childFile('CMakeLists.txt'); + /// Eventually this will be used to check if the user's unstable project needs to be regenerated. int get projectVersion => int.tryParse(_editableDirectory.childFile('project_version').readAsStringSync()); + + /// Retrieve the GUID of the UWP package. + String get packageGuid => _packageGuid ??= getCmakePackageGuid(runnerCmakeFile); + String _packageGuid; + + File get appManifest => _editableDirectory.childDirectory('runner_uwp').childFile('appxmanifest.in'); + + String get packageVersion => _packageVersion ??= parseAppVersion(this); + String _packageVersion; +} + +@visibleForTesting +String parseAppVersion(WindowsUwpProject project) { + final File appManifestFile = project.appManifest; + if (!appManifestFile.existsSync()) { + return null; + } + + XmlDocument document; + try { + document = XmlDocument.parse(appManifestFile.readAsStringSync()); + } on XmlParserException { + throwToolExit('Error parsing $appManifestFile. Please ensure that the appx manifest is a valid XML document and try again.'); + } + for (final XmlElement metaData in document.findAllElements('Identity')) { + return metaData.getAttribute('Version'); + } + return null; } /// The Linux sub project. diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index fb8f43d03b8..ff8bf1fef69 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -1466,7 +1466,9 @@ DevelopmentArtifact artifactFromTargetPlatform(TargetPlatform targetPlatform) { case TargetPlatform.fuchsia_x64: case TargetPlatform.tester: case TargetPlatform.windows_uwp_x64: - // No artifacts currently supported. + if (featureFlags.isWindowsUwpEnabled) { + return DevelopmentArtifact.windowsUwp; + } return null; } return null; diff --git a/packages/flutter_tools/lib/src/windows/application_package.dart b/packages/flutter_tools/lib/src/windows/application_package.dart index f0961b06d2e..de5e2129edd 100644 --- a/packages/flutter_tools/lib/src/windows/application_package.dart +++ b/packages/flutter_tools/lib/src/windows/application_package.dart @@ -75,3 +75,14 @@ class BuildableWindowsApp extends WindowsApp { @override String get name => project.parent.manifest.appName; } + +class BuildableUwpApp extends ApplicationPackage { + BuildableUwpApp({@required this.project}) : super(id: project.packageGuid); + + final WindowsUwpProject project; + + String get projectVersion => project.packageVersion; + + @override + String get name => getCmakeExecutableName(project); +} diff --git a/packages/flutter_tools/lib/src/windows/uwptool.dart b/packages/flutter_tools/lib/src/windows/uwptool.dart new file mode 100644 index 00000000000..e7522365b1f --- /dev/null +++ b/packages/flutter_tools/lib/src/windows/uwptool.dart @@ -0,0 +1,87 @@ +// 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. + +// @dart = 2.8 + +import 'dart:async'; + +import 'package:meta/meta.dart'; +import 'package:process/process.dart'; + +import '../artifacts.dart'; +import '../base/logger.dart'; +import '../base/process.dart'; + +/// The uwptool command-line tool. +/// +/// `uwptool` is a host utility command-line tool that supports a variety of +/// actions related to Universal Windows Platform (UWP) applications, including +/// installing and uninstalling apps, querying installed apps, and launching +/// apps. +class UwpTool { + UwpTool({ + @required Artifacts artifacts, + @required Logger logger, + @required ProcessManager processManager, + }) : _artifacts = artifacts, + _logger = logger, + _processUtils = ProcessUtils(processManager: processManager, logger: logger); + + final Artifacts _artifacts; + final Logger _logger; + final ProcessUtils _processUtils; + + String get _binaryPath => _artifacts.getArtifactPath(Artifact.uwptool); + + Future> listApps() async { + final List launchCommand = [ + _binaryPath, + 'listapps', + ]; + final RunResult result = await _processUtils.run(launchCommand); + if (result.exitCode != 0) { + _logger.printError('Failed to list installed UWP apps: ${result.stderr}'); + return []; + } + final List appIds = []; + for (final String line in result.stdout.toString().split('\n')) { + final String appId = line.trim(); + if (appId.isNotEmpty) { + appIds.add(appId); + } + } + return appIds; + } + + /// Returns the app ID for the specified package ID. + /// + /// If no installed application on the system matches the specified GUID, + /// returns null. + Future getAppIdFromPackageId(String packageId) async { + for (final String appId in await listApps()) { + if (appId.startsWith(packageId)) { + return appId; + } + } + return null; + } + + /// Launches the app with the specified app ID. + /// + /// On success, returns the process ID of the launched app, otherwise null. + Future launchApp(String appId, List args) async { + final List launchCommand = [ + _binaryPath, + 'launch', + appId + ] + args; + final RunResult result = await _processUtils.run(launchCommand); + if (result.exitCode != 0) { + _logger.printError('Failed to launch app $appId: ${result.stderr}'); + return null; + } + // Read the process ID from stdout. + return int.tryParse(result.stdout.toString().trim()); + } +} diff --git a/packages/flutter_tools/lib/src/windows/windows_device.dart b/packages/flutter_tools/lib/src/windows/windows_device.dart index aa4a3b1c949..b6577397ed5 100644 --- a/packages/flutter_tools/lib/src/windows/windows_device.dart +++ b/packages/flutter_tools/lib/src/windows/windows_device.dart @@ -4,20 +4,26 @@ // @dart = 2.8 +import 'dart:async'; + import 'package:meta/meta.dart'; import 'package:process/process.dart'; import '../application_package.dart'; import '../base/file_system.dart'; +import '../base/io.dart'; import '../base/logger.dart'; import '../base/os.dart'; +import '../base/utils.dart'; import '../build_info.dart'; import '../desktop_device.dart'; import '../device.dart'; +import '../device_port_forwarder.dart'; import '../features.dart'; import '../project.dart'; import 'application_package.dart'; import 'build_windows.dart'; +import 'uwptool.dart'; import 'windows_workflow.dart'; /// A device that represents a desktop Windows target. @@ -71,21 +77,33 @@ class WindowsDevice extends DesktopDevice { } // A device that represents a desktop Windows UWP target. -class WindowsUWPDevice extends DesktopDevice { +class WindowsUWPDevice extends Device { WindowsUWPDevice({ @required ProcessManager processManager, @required Logger logger, @required FileSystem fileSystem, @required OperatingSystemUtils operatingSystemUtils, - }) : super( - 'winuwp', - platformType: PlatformType.windows, - ephemeral: false, - processManager: processManager, - logger: logger, - fileSystem: fileSystem, - operatingSystemUtils: operatingSystemUtils, - ); + @required UwpTool uwptool, + }) : _logger = logger, + _processManager = processManager, + _operatingSystemUtils = operatingSystemUtils, + _fileSystem = fileSystem, + _uwptool = uwptool, + super( + 'winuwp', + platformType: PlatformType.windows, + ephemeral: false, + category: Category.desktop, + ); + + final ProcessManager _processManager; + final Logger _logger; + final FileSystem _fileSystem; + final OperatingSystemUtils _operatingSystemUtils; + final UwpTool _uwptool; + BuildMode _buildMode; + + int _processId; @override bool isSupported() => true; @@ -98,29 +116,150 @@ class WindowsUWPDevice extends DesktopDevice { @override bool isSupportedForProject(FlutterProject flutterProject) { - // TODO(flutter): update with detection once FlutterProject knows - // about the UWP structure. - return true; + return flutterProject.windowsUwp.existsSync(); } @override - Future buildForDevice( - covariant WindowsApp package, { + void clearLogs() { } + + @override + Future dispose() async { } + + @override + Future get emulatorId => null; + + @override + FutureOr getLogReader({covariant BuildableUwpApp app, bool includePastLogs = false}) { + return NoOpDeviceLogReader('winuwp'); + } + + @override + Future installApp(covariant BuildableUwpApp app, {String userIdentifier}) async { + /// The cmake build generates an install powershell script. + /// build\winuwp\runner_uwp\AppPackages\\__\Add-AppDevPackage.ps1 + final String binaryName = app.name; + final String packageVersion = app.projectVersion; + if (packageVersion == null) { + return false; + } + final String config = toTitleCase(getNameForBuildMode(_buildMode ?? BuildMode.debug)); + final String generated = '${binaryName}_${packageVersion}_${config}_Test'; + final ProcessResult result = await _processManager.run([ + 'powershell.exe', + _fileSystem.path.join('build', 'winuwp', 'runner_uwp', 'AppPackages', binaryName, generated, 'install.ps1'), + ]); + if (result.exitCode != 0) { + _logger.printError(result.stdout.toString()); + _logger.printError(result.stderr.toString()); + } + return result.exitCode == 0; + } + + @override + Future isAppInstalled(covariant ApplicationPackage app, {String userIdentifier}) async => false; + + @override + Future isLatestBuildInstalled(covariant ApplicationPackage app) async => false; + + @override + Future get isLocalEmulator async => false; + + @override + DevicePortForwarder get portForwarder => const NoOpDevicePortForwarder(); + + @override + Future get sdkNameAndVersion async => ''; + + @override + Future startApp(covariant BuildableUwpApp package, { String mainPath, - BuildInfo buildInfo, + String route, + DebuggingOptions debuggingOptions, + Map platformArgs, + bool prebuiltApplication = false, + bool ipv6 = false, + String userIdentifier, }) async { - await buildWindowsUwp( - FlutterProject.current().windowsUwp, - buildInfo, - target: mainPath, - ); + _buildMode = debuggingOptions.buildInfo.mode; + if (!prebuiltApplication) { + await buildWindowsUwp( + package.project, + debuggingOptions.buildInfo, + target: mainPath, + ); + } + if (!await isAppInstalled(package) && !await installApp(package)) { + _logger.printError('Failed to install app package'); + return LaunchResult.failed(); + } + + final String guid = package.id; + if (guid == null) { + _logger.printError('Could not find PACKAGE_GUID in ${package.project.runnerCmakeFile.path}'); + return LaunchResult.failed(); + } + + final String appId = await _uwptool.getAppIdFromPackageId(guid); + + if (debuggingOptions.buildInfo.mode.isRelease) { + _processId = await _uwptool.launchApp(appId, []); + return _processId != null ? LaunchResult.succeeded() : LaunchResult.failed(); + } + + /// If the terminal is attached, prompt the user to open the firewall port. + if (_logger.terminal.stdinHasTerminal) { + await _logger.terminal.promptForCharInput(['Y', 'y'], logger: _logger, + prompt: 'To continue start an admin cmd prompt and run the following command:\n' + ' checknetisolation loopbackexempt -is -n=$appId\n' + 'Press "Y/y" once this is complete.' + ); + } + + /// Currently we do not have a way to discover the VM Service URI. + final int port = debuggingOptions.deviceVmServicePort ?? await _operatingSystemUtils.findFreePort(); + final List args = [ + '--observatory-port=$port', + '--disable-service-auth-codes', + '--enable-dart-profiling', + if (debuggingOptions.startPaused) '--start-paused', + if (debuggingOptions.useTestFonts) '--use-test-fonts', + if (debuggingOptions.debuggingEnabled) ...[ + '--enable-checked-mode', + '--verify-entry-points', + ], + if (debuggingOptions.enableSoftwareRendering) '--enable-software-rendering', + if (debuggingOptions.skiaDeterministicRendering) '--skia-deterministic-rendering', + if (debuggingOptions.traceSkia) '--trace-skia', + if (debuggingOptions.traceAllowlist != null) '--trace-allowlist="${debuggingOptions.traceAllowlist}"', + if (debuggingOptions.endlessTraceBuffer) '--endless-trace-buffer', + if (debuggingOptions.dumpSkpOnShaderCompilation) '--dump-skp-on-shader-compilation', + if (debuggingOptions.verboseSystemLogs) '--verbose-logging', + if (debuggingOptions.cacheSkSL) '--cache-sksl', + if (debuggingOptions.purgePersistentCache) '--purge-persistent-cache', + if (platformArgs['trace-startup'] as bool ?? false) '--trace-startup', + ]; + _processId = await _uwptool.launchApp(appId, args); + if (_processId == null) { + return LaunchResult.failed(); + } + return LaunchResult.succeeded(observatoryUri: Uri.parse('http://localhost:$port')); } @override - String executablePathForDevice(ApplicationPackage package, BuildMode buildMode) { - // TODO(flutter): update once application package factory knows about UWP bundle. - throw UnsupportedError('Windows UWP device not implemented.'); + Future stopApp(covariant BuildableUwpApp app, {String userIdentifier}) async { + if (_processId != null) { + return _processManager.killPid(_processId); + } + return false; } + + @override + Future uninstallApp(covariant BuildableUwpApp app, {String userIdentifier}) async { + return false; + } + + @override + FutureOr supportsRuntimeMode(BuildMode buildMode) => buildMode != BuildMode.jitRelease; } class WindowsDevices extends PollingDeviceDiscovery { @@ -131,12 +270,14 @@ class WindowsDevices extends PollingDeviceDiscovery { @required OperatingSystemUtils operatingSystemUtils, @required WindowsWorkflow windowsWorkflow, @required FeatureFlags featureFlags, + @required UwpTool uwptool, }) : _fileSystem = fileSystem, _logger = logger, _processManager = processManager, _operatingSystemUtils = operatingSystemUtils, _windowsWorkflow = windowsWorkflow, _featureFlags = featureFlags, + _uwptool = uwptool, super('windows devices'); final FileSystem _fileSystem; @@ -145,6 +286,7 @@ class WindowsDevices extends PollingDeviceDiscovery { final OperatingSystemUtils _operatingSystemUtils; final WindowsWorkflow _windowsWorkflow; final FeatureFlags _featureFlags; + final UwpTool _uwptool; @override bool get supportsPlatform => _windowsWorkflow.appliesToHostPlatform; @@ -170,6 +312,7 @@ class WindowsDevices extends PollingDeviceDiscovery { logger: _logger, processManager: _processManager, operatingSystemUtils: _operatingSystemUtils, + uwptool: _uwptool, ) ]; } diff --git a/packages/flutter_tools/test/general.shard/windows/project_test.dart b/packages/flutter_tools/test/general.shard/windows/project_test.dart new file mode 100644 index 00000000000..9b907c4754a --- /dev/null +++ b/packages/flutter_tools/test/general.shard/windows/project_test.dart @@ -0,0 +1,140 @@ +// 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. + +// @dart = 2.8 + +import 'package:file/file.dart'; +import 'package:file/memory.dart'; +import 'package:flutter_tools/src/project.dart'; + +import '../../src/common.dart'; + +const String kExampleManifest = r''' + + + + + + + + @SHORT_NAME@ + CMake Test Cert + Assets/StoreLogo.png + + + + + + + + + + + + + + + + + + + + + + +'''; + +void main() { + testWithoutContext('Project can parse the app version from the appx manifest', () { + final FileSystem fileSystem = MemoryFileSystem.test(); + fileSystem.file('winuwp/runner_uwp/appxmanifest.in') + ..createSync(recursive: true) + ..writeAsStringSync(kExampleManifest); + + final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + + expect(flutterProject.windowsUwp.packageVersion, '2.3.1.4'); + }); + + testWithoutContext('Project returns null if appx manifest does not exist', () { + final FileSystem fileSystem = MemoryFileSystem.test(); + + final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + + expect(flutterProject.windowsUwp.packageVersion, null); + }); + + testWithoutContext('Project throws a tool exit if appxmanifest is not valid xml', () { + final FileSystem fileSystem = MemoryFileSystem.test(); + fileSystem.file('winuwp/runner_uwp/appxmanifest.in') + ..createSync(recursive: true) + ..writeAsStringSync('['); + + final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + + expect(() => flutterProject.windowsUwp.packageVersion, throwsToolExit()); + }); + + testWithoutContext('Can parse the PACKAGE_GUID from the Cmake manifest', () { + final FileSystem fileSystem = MemoryFileSystem.test(); + fileSystem.file('winuwp/runner_uwp/CMakeLists.txt') + ..createSync(recursive: true) + ..writeAsStringSync(r''' +cmake_minimum_required (VERSION 3.8) +set(CMAKE_SYSTEM_NAME WindowsStore) +set(CMAKE_SYSTEM_VERSION 10.0) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED YES) + +include(CMakePrintHelpers) + +project ("TestBedUWP") + +set(APP_MANIFEST_NAME Package.appxmanifest) +set(APP_MANIFEST_TARGET_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/${APP_MANIFEST_NAME}) +set(SHORT_NAME ${BINARY_NAME}) +set(PACKAGE_GUID "F941A77F-8AE1-4E3E-9611-68FBD3C62AE8") + +'''); + + final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + + expect(flutterProject.windowsUwp.packageGuid, 'F941A77F-8AE1-4E3E-9611-68FBD3C62AE8'); + }); + + testWithoutContext('Returns null if the PACKAGE_GUID cannot be found in the Cmake file', () { + final FileSystem fileSystem = MemoryFileSystem.test(); + fileSystem.file('winuwp/runner_uwp/CMakeLists.txt') + ..createSync(recursive: true) + ..writeAsStringSync(r''' +cmake_minimum_required (VERSION 3.8) +set(CMAKE_SYSTEM_NAME WindowsStore) +set(CMAKE_SYSTEM_VERSION 10.0) +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED YES) + +include(CMakePrintHelpers) + +project ("TestBedUWP") + +set(APP_MANIFEST_NAME Package.appxmanifest) +set(APP_MANIFEST_TARGET_LOCATION ${CMAKE_CURRENT_BINARY_DIR}/${APP_MANIFEST_NAME}) +set(SHORT_NAME ${BINARY_NAME}) +'''); + + final FlutterProject flutterProject = FlutterProject.fromDirectoryTest(fileSystem.currentDirectory); + + expect(flutterProject.windowsUwp.packageGuid, null); + }); +} diff --git a/packages/flutter_tools/test/general.shard/windows/windows_device_test.dart b/packages/flutter_tools/test/general.shard/windows/windows_device_test.dart index f145b5e1f48..41637a8c50b 100644 --- a/packages/flutter_tools/test/general.shard/windows/windows_device_test.dart +++ b/packages/flutter_tools/test/general.shard/windows/windows_device_test.dart @@ -9,10 +9,12 @@ 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/build_info.dart'; +import 'package:flutter_tools/src/cache.dart'; import 'package:flutter_tools/src/device.dart'; import 'package:flutter_tools/src/features.dart'; import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/windows/application_package.dart'; +import 'package:flutter_tools/src/windows/uwptool.dart'; import 'package:flutter_tools/src/windows/windows_device.dart'; import 'package:flutter_tools/src/windows/windows_workflow.dart'; import 'package:test/fake.dart'; @@ -42,14 +44,14 @@ void main() { testWithoutContext('WindowsUwpDevice defaults', () async { final WindowsUWPDevice windowsDevice = setUpWindowsUwpDevice(); - final PrebuiltWindowsApp windowsApp = PrebuiltWindowsApp(executable: 'foo'); + final FakeBuildableUwpApp package = FakeBuildableUwpApp(); expect(await windowsDevice.targetPlatform, TargetPlatform.windows_uwp_x64); expect(windowsDevice.name, 'Windows (UWP)'); - expect(await windowsDevice.installApp(windowsApp), true); - expect(await windowsDevice.uninstallApp(windowsApp), true); - expect(await windowsDevice.isLatestBuildInstalled(windowsApp), true); - expect(await windowsDevice.isAppInstalled(windowsApp), true); + expect(await windowsDevice.installApp(package), true); + expect(await windowsDevice.uninstallApp(package), false); + expect(await windowsDevice.isLatestBuildInstalled(package), false); + expect(await windowsDevice.isAppInstalled(package), false); expect(windowsDevice.category, Category.desktop); expect(windowsDevice.supportsRuntimeMode(BuildMode.debug), true); @@ -69,6 +71,7 @@ void main() { logger: BufferLogger.test(), processManager: FakeProcessManager.any(), fileSystem: MemoryFileSystem.test(), + uwptool: FakeUwpTool(), ).devices, []); }); @@ -83,6 +86,7 @@ void main() { processManager: FakeProcessManager.any(), fileSystem: MemoryFileSystem.test(), featureFlags: TestFeatureFlags(isWindowsEnabled: true), + uwptool: FakeUwpTool(), ).devices, hasLength(1)); }); @@ -98,6 +102,7 @@ void main() { processManager: FakeProcessManager.any(), fileSystem: MemoryFileSystem.test(), featureFlags: featureFlags, + uwptool: FakeUwpTool(), ).devices, hasLength(2)); }); @@ -112,6 +117,7 @@ void main() { processManager: FakeProcessManager.any(), fileSystem: MemoryFileSystem.test(), featureFlags: TestFeatureFlags(isWindowsEnabled: true), + uwptool: FakeUwpTool(), ); // Timeout ignored. final List devices = await windowsDevices.discoverDevices(timeout: const Duration(seconds: 10)); @@ -159,6 +165,70 @@ void main() { expect(windowsDevice.executablePathForDevice(fakeApp, BuildMode.profile), 'profile/executable'); expect(windowsDevice.executablePathForDevice(fakeApp, BuildMode.release), 'release/executable'); }); + + testWithoutContext('WinUWPDevice can launch application', () async { + Cache.flutterRoot = ''; + final FakeUwpTool uwptool = FakeUwpTool(); + final FileSystem fileSystem = MemoryFileSystem.test(); + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand(command: [ + 'powershell.exe', + 'build/winuwp/runner_uwp/AppPackages/testapp/testapp_1.2.3.4_Debug_Test/install.ps1', + ]), + ]); + final WindowsUWPDevice windowsDevice = setUpWindowsUwpDevice( + fileSystem: fileSystem, + processManager: processManager, + uwptool: uwptool, + ); + final FakeBuildableUwpApp package = FakeBuildableUwpApp(); + + final LaunchResult result = await windowsDevice.startApp( + package, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug), + prebuiltApplication: true, + platformArgs: {}, + ); + + expect(result.started, true); + expect(uwptool.launchRequests.single.appId, 'PACKAGE-ID_asdfghjkl'); + expect(uwptool.launchRequests.single.args, [ + '--observatory-port=12345', + '--disable-service-auth-codes', + '--enable-dart-profiling', + '--enable-checked-mode', + '--verify-entry-points', + ]); + }); + + testWithoutContext('WinUWPDevice can launch application in release mode', () async { + Cache.flutterRoot = ''; + final FakeUwpTool uwptool = FakeUwpTool(); + final FileSystem fileSystem = MemoryFileSystem.test(); + final FakeProcessManager processManager = FakeProcessManager.list([ + const FakeCommand(command: [ + 'powershell.exe', + 'build/winuwp/runner_uwp/AppPackages/testapp/testapp_1.2.3.4_Release_Test/install.ps1', + ]), + ]); + final WindowsUWPDevice windowsDevice = setUpWindowsUwpDevice( + fileSystem: fileSystem, + processManager: processManager, + uwptool: uwptool, + ); + final FakeBuildableUwpApp package = FakeBuildableUwpApp(); + + final LaunchResult result = await windowsDevice.startApp( + package, + debuggingOptions: DebuggingOptions.enabled(BuildInfo.release), + prebuiltApplication: true, + platformArgs: {}, + ); + + expect(result.started, true); + expect(uwptool.launchRequests.single.appId, 'PACKAGE-ID_asdfghjkl'); + expect(uwptool.launchRequests.single.args, []); + }); } FlutterProject setUpFlutterProject(Directory directory) { @@ -186,12 +256,14 @@ WindowsUWPDevice setUpWindowsUwpDevice({ FileSystem fileSystem, Logger logger, ProcessManager processManager, + UwpTool uwptool, }) { return WindowsUWPDevice( fileSystem: fileSystem ?? MemoryFileSystem.test(), logger: logger ?? BufferLogger.test(), processManager: processManager ?? FakeProcessManager.any(), operatingSystemUtils: FakeOperatingSystemUtils(), + uwptool: uwptool ?? FakeUwpTool(), ); } @@ -199,3 +271,52 @@ class FakeWindowsApp extends Fake implements WindowsApp { @override String executable(BuildMode buildMode) => '${buildMode.name}/executable'; } + +class FakeBuildableUwpApp extends Fake implements BuildableUwpApp { + @override + String get id => 'PACKAGE-ID'; + @override + String get name => 'testapp'; + @override + String get projectVersion => '1.2.3.4'; +} + +class FakeUwpTool implements UwpTool { + final List<_LaunchRequest> launchRequests = <_LaunchRequest>[]; + final List<_LookupAppIdRequest> lookupAppIdRequests = <_LookupAppIdRequest>[]; + + @override + Future> listApps() async { + return [ + 'fb89bf4f-55db-4bcd-8f0b-d8139953e08b', + '3e556a66-cb7f-4335-9569-35d5f5e37219', + 'dfe5d409-a524-4635-b2f8-78a5e9551994', + '51e8a06b-02e8-4f76-9131-f20ce114fc34', + ]; + } + + @override + Future getAppIdFromPackageId(String packageId) async { + lookupAppIdRequests.add(_LookupAppIdRequest(packageId)); + return '${packageId}_asdfghjkl'; + } + + @override + Future launchApp(String appId, List args) async { + launchRequests.add(_LaunchRequest(appId, args)); + return 42; + } +} + +class _LookupAppIdRequest { + const _LookupAppIdRequest(this.packageId); + + final String packageId; +} + +class _LaunchRequest { + const _LaunchRequest(this.appId, this.args); + + final String appId; + final List args; +}