From d745eec0514ffdbe35a78c15949a694aea6a0ffb Mon Sep 17 00:00:00 2001 From: Jenn Magder Date: Wed, 6 Apr 2022 13:17:26 -0700 Subject: [PATCH] Add --use-application-binary to "flutter install" (#101324) --- .../lib/src/commands/build_ios.dart | 4 +- .../flutter_tools/lib/src/commands/drive.dart | 9 ++-- .../lib/src/commands/install.dart | 9 ++++ .../flutter_tools/lib/src/commands/run.dart | 15 +++---- .../lib/src/runner/flutter_command.dart | 11 +++++ .../commands.shard/hermetic/install_test.dart | 44 +++++++++++++++++++ 6 files changed, 76 insertions(+), 16 deletions(-) diff --git a/packages/flutter_tools/lib/src/commands/build_ios.dart b/packages/flutter_tools/lib/src/commands/build_ios.dart index 5607ff6a38c..704c808de83 100644 --- a/packages/flutter_tools/lib/src/commands/build_ios.dart +++ b/packages/flutter_tools/lib/src/commands/build_ios.dart @@ -42,7 +42,7 @@ class BuildIOSCommand extends _BuildIOSSubCommand { final String name = 'ios'; @override - final String description = 'Build an iOS application bundle (Mac OS X host only).'; + final String description = 'Build an iOS application bundle (macOS host only).'; @override final XcodeBuildAction xcodeBuildAction = XcodeBuildAction.build; @@ -94,7 +94,7 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { final List aliases = ['xcarchive']; @override - final String description = 'Build an iOS archive bundle and IPA for distribution (Mac OS X host only).'; + final String description = 'Build an iOS archive bundle and IPA for distribution (macOS host only).'; @override final XcodeBuildAction xcodeBuildAction = XcodeBuildAction.archive; diff --git a/packages/flutter_tools/lib/src/commands/drive.dart b/packages/flutter_tools/lib/src/commands/drive.dart index f77b3d0afd5..1074a4bdc04 100644 --- a/packages/flutter_tools/lib/src/commands/drive.dart +++ b/packages/flutter_tools/lib/src/commands/drive.dart @@ -92,7 +92,7 @@ class DriveCommand extends RunCommandBase { ) ..addFlag('build', defaultsTo: true, - help: '(deprecated) Build the app before running. To use an existing app, pass the "--use-application-binary" ' + help: '(deprecated) Build the app before running. To use an existing app, pass the "--${FlutterOptions.kUseApplicationBinary}" ' 'flag with an existing APK.', ) ..addOption('screenshot', @@ -209,7 +209,8 @@ class DriveCommand extends RunCommandBase { if (await _fileSystem.type(testFile) != FileSystemEntityType.file) { throwToolExit('Test file not found: $testFile'); } - final Device device = await findTargetDevice(includeUnsupportedDevices: stringArg('use-application-binary') == null); + final String applicationBinaryPath = stringArg(FlutterOptions.kUseApplicationBinary); + final Device device = await findTargetDevice(includeUnsupportedDevices: applicationBinaryPath == null); if (device == null) { throwToolExit(null); } @@ -233,9 +234,9 @@ class DriveCommand extends RunCommandBase { final DriverService driverService = _flutterDriverFactory.createDriverService(web); final BuildInfo buildInfo = await getBuildInfo(); final DebuggingOptions debuggingOptions = await createDebuggingOptions(web); - final File applicationBinary = stringArg('use-application-binary') == null + final File applicationBinary = applicationBinaryPath == null ? null - : _fileSystem.file(stringArg('use-application-binary')); + : _fileSystem.file(applicationBinaryPath); bool screenshotTaken = false; try { diff --git a/packages/flutter_tools/lib/src/commands/install.dart b/packages/flutter_tools/lib/src/commands/install.dart index 9ad0aa57bfc..77cd3b1cf04 100644 --- a/packages/flutter_tools/lib/src/commands/install.dart +++ b/packages/flutter_tools/lib/src/commands/install.dart @@ -5,6 +5,7 @@ import '../android/android_device.dart'; import '../application_package.dart'; import '../base/common.dart'; +import '../base/file_system.dart'; import '../base/io.dart'; import '../device.dart'; import '../globals.dart' as globals; @@ -15,6 +16,7 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts requiresPubspecYaml(); usesDeviceUserOption(); usesDeviceTimeoutOption(); + usesApplicationBinaryOption(); argParser.addFlag('uninstall-only', help: 'Uninstall the app if already on the device. Skip install.', ); @@ -34,6 +36,9 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts bool get uninstallOnly => boolArg('uninstall-only'); String? get userIdentifier => stringArg(FlutterOptions.kDeviceUser); + String? get _applicationBinaryPath => stringArg(FlutterOptions.kUseApplicationBinary); + File? get _applicationBinary => _applicationBinaryPath == null ? null : globals.fs.file(_applicationBinaryPath); + @override Future validateCommand() async { await super.validateCommand(); @@ -44,6 +49,9 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts if (userIdentifier != null && device is! AndroidDevice) { throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android'); } + if (_applicationBinaryPath != null && !(_applicationBinary?.existsSync() ?? true)) { + throwToolExit('Prebuilt binary $_applicationBinaryPath does not exist'); + } } @override @@ -51,6 +59,7 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts final Device targetDevice = device!; final ApplicationPackage? package = await applicationPackages?.getPackageForPlatform( await targetDevice.targetPlatform, + applicationBinary: _applicationBinary, ); if (package == null) { throwToolExit('Could not find or build package'); diff --git a/packages/flutter_tools/lib/src/commands/run.dart b/packages/flutter_tools/lib/src/commands/run.dart index 5d00f13525a..2307317410f 100644 --- a/packages/flutter_tools/lib/src/commands/run.dart +++ b/packages/flutter_tools/lib/src/commands/run.dart @@ -38,6 +38,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment usesWebRendererOption(); addNativeNullAssertions(hide: !verboseHelp); addBundleSkSLPathOption(hide: !verboseHelp); + usesApplicationBinaryOption(); argParser ..addFlag('trace-startup', negatable: false, @@ -87,12 +88,6 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment help: '(deprecated) Allow connections to the VM service without using authentication codes. ' '(Not recommended! This can open your device to remote code execution attacks!)' ) - ..addOption('use-application-binary', - help: 'Specify a pre-built application binary to use when running. For Android applications, ' - 'this must be the path to an APK. For iOS applications, the path to an IPA. Other device types ' - 'do not yet support prebuilt application binaries.', - valueHelp: 'path/to/app.apk', - ) ..addFlag('start-paused', defaultsTo: startPausedDefault, help: 'Start in a paused mode and wait for a debugger to connect.', @@ -168,7 +163,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment bool get purgePersistentCache => boolArg('purge-persistent-cache'); bool get disableServiceAuthCodes => boolArg('disable-service-auth-codes'); bool get cacheStartupProfile => boolArg('cache-startup-profile'); - bool get runningWithPrebuiltApplication => argResults['use-application-binary'] != null; + bool get runningWithPrebuiltApplication => argResults[FlutterOptions.kUseApplicationBinary] != null; bool get trackWidgetCreation => boolArg('track-widget-creation'); bool get enableImpeller => boolArg('enable-impeller'); @@ -347,7 +342,7 @@ class RunCommand extends RunCommandBase { defaultsTo: false, 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".', + 'cannot be paired with "--${FlutterOptions.kUseApplicationBinary}".', hide: !verboseHelp, ); } @@ -484,7 +479,7 @@ class RunCommand extends RunCommandBase { throwToolExit(null); } if (globals.deviceManager.hasSpecifiedAllDevices && runningWithPrebuiltApplication) { - throwToolExit('Using "-d all" with "--use-application-binary" is not supported'); + throwToolExit('Using "-d all" with "--${FlutterOptions.kUseApplicationBinary}" is not supported'); } if (userIdentifier != null @@ -562,7 +557,7 @@ class RunCommand extends RunCommandBase { // debug mode. final BuildInfo buildInfo = await getBuildInfo(); final bool hotMode = shouldUseHotMode(buildInfo); - final String applicationBinaryPath = stringArg('use-application-binary'); + final String applicationBinaryPath = stringArg(FlutterOptions.kUseApplicationBinary); if (boolArg('machine')) { if (devices.length > 1) { diff --git a/packages/flutter_tools/lib/src/runner/flutter_command.dart b/packages/flutter_tools/lib/src/runner/flutter_command.dart index 6f11248bc82..b72b8644f63 100644 --- a/packages/flutter_tools/lib/src/runner/flutter_command.dart +++ b/packages/flutter_tools/lib/src/runner/flutter_command.dart @@ -117,6 +117,7 @@ class FlutterOptions { static const String kInitializeFromDill = 'initialize-from-dill'; static const String kAssumeInitializeFromDillUpToDate = 'assume-initialize-from-dill-up-to-date'; static const String kFatalWarnings = 'fatal-warnings'; + static const String kUseApplicationBinary = 'use-application-binary'; } /// flutter command categories for usage. @@ -606,6 +607,16 @@ abstract class FlutterCommand extends Command { ); } + void usesApplicationBinaryOption() { + argParser.addOption( + FlutterOptions.kUseApplicationBinary, + help: 'Specify a pre-built application binary to use when running. For Android applications, ' + 'this must be the path to an APK. For iOS applications, the path to an IPA. Other device types ' + 'do not yet support prebuilt application binaries.', + valueHelp: 'path/to/app.apk', + ); + } + /// Whether it is safe for this command to use a cached pub invocation. bool get cachePubGet => true; diff --git a/packages/flutter_tools/test/commands.shard/hermetic/install_test.dart b/packages/flutter_tools/test/commands.shard/hermetic/install_test.dart index aff27ab2d48..494e6b57f1a 100644 --- a/packages/flutter_tools/test/commands.shard/hermetic/install_test.dart +++ b/packages/flutter_tools/test/commands.shard/hermetic/install_test.dart @@ -5,6 +5,7 @@ // @dart = 2.8 import 'package:file/file.dart'; +import 'package:file/memory.dart'; import 'package:flutter_tools/src/android/android_device.dart'; import 'package:flutter_tools/src/android/application_package.dart'; import 'package:flutter_tools/src/application_package.dart'; @@ -18,6 +19,7 @@ import 'package:test/fake.dart'; import '../../src/common.dart'; import '../../src/context.dart'; +import '../../src/fake_process_manager.dart'; import '../../src/test_flutter_command_runner.dart'; void main() { @@ -26,6 +28,12 @@ void main() { Cache.disableLocking(); }); + FileSystem fileSystem; + setUp(() { + fileSystem = MemoryFileSystem.test(); + fileSystem.file('pubspec.yaml').createSync(recursive: true); + }); + testUsingContext('returns 0 when Android is connected and ready for an install', () async { final InstallCommand command = InstallCommand(); command.applicationPackages = FakeApplicationPackageFactory(FakeAndroidApk()); @@ -36,6 +44,8 @@ void main() { await createTestCommandRunner(command).run(['install']); }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('returns 1 when targeted device is not Android with --device-user', () async { @@ -49,6 +59,8 @@ void main() { throwsToolExit(message: '--device-user is only supported for Android')); }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), }); testUsingContext('returns 0 when iOS is connected and ready for an install', () async { @@ -61,6 +73,38 @@ void main() { await createTestCommandRunner(command).run(['install']); }, overrides: { Cache: () => Cache.test(processManager: FakeProcessManager.any()), + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('fails when prebuilt binary not found', () async { + final InstallCommand command = InstallCommand(); + command.applicationPackages = FakeApplicationPackageFactory(FakeAndroidApk()); + + final FakeAndroidDevice device = FakeAndroidDevice(); + testDeviceManager.addDevice(device); + + expect(() async => createTestCommandRunner(command).run(['install', '--use-application-binary', 'bogus']), + throwsToolExit(message: 'Prebuilt binary bogus does not exist')); + }, overrides: { + Cache: () => Cache.test(processManager: FakeProcessManager.any()), + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), + }); + + testUsingContext('succeeds using prebuilt binary', () async { + final InstallCommand command = InstallCommand(); + command.applicationPackages = FakeApplicationPackageFactory(FakeAndroidApk()); + + final FakeAndroidDevice device = FakeAndroidDevice(); + testDeviceManager.addDevice(device); + fileSystem.file('binary').createSync(recursive: true); + + await createTestCommandRunner(command).run(['install', '--use-application-binary', 'binary']); + }, overrides: { + Cache: () => Cache.test(processManager: FakeProcessManager.any()), + FileSystem: () => fileSystem, + ProcessManager: () => FakeProcessManager.any(), }); }); }