Unverified Commit d745eec0 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Add --use-application-binary to "flutter install" (#101324)

parent d08f7ab9
...@@ -42,7 +42,7 @@ class BuildIOSCommand extends _BuildIOSSubCommand { ...@@ -42,7 +42,7 @@ class BuildIOSCommand extends _BuildIOSSubCommand {
final String name = 'ios'; final String name = 'ios';
@override @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 @override
final XcodeBuildAction xcodeBuildAction = XcodeBuildAction.build; final XcodeBuildAction xcodeBuildAction = XcodeBuildAction.build;
...@@ -94,7 +94,7 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand { ...@@ -94,7 +94,7 @@ class BuildIOSArchiveCommand extends _BuildIOSSubCommand {
final List<String> aliases = <String>['xcarchive']; final List<String> aliases = <String>['xcarchive'];
@override @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 @override
final XcodeBuildAction xcodeBuildAction = XcodeBuildAction.archive; final XcodeBuildAction xcodeBuildAction = XcodeBuildAction.archive;
......
...@@ -92,7 +92,7 @@ class DriveCommand extends RunCommandBase { ...@@ -92,7 +92,7 @@ class DriveCommand extends RunCommandBase {
) )
..addFlag('build', ..addFlag('build',
defaultsTo: true, 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.', 'flag with an existing APK.',
) )
..addOption('screenshot', ..addOption('screenshot',
...@@ -209,7 +209,8 @@ class DriveCommand extends RunCommandBase { ...@@ -209,7 +209,8 @@ class DriveCommand extends RunCommandBase {
if (await _fileSystem.type(testFile) != FileSystemEntityType.file) { if (await _fileSystem.type(testFile) != FileSystemEntityType.file) {
throwToolExit('Test file not found: $testFile'); 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) { if (device == null) {
throwToolExit(null); throwToolExit(null);
} }
...@@ -233,9 +234,9 @@ class DriveCommand extends RunCommandBase { ...@@ -233,9 +234,9 @@ class DriveCommand extends RunCommandBase {
final DriverService driverService = _flutterDriverFactory.createDriverService(web); final DriverService driverService = _flutterDriverFactory.createDriverService(web);
final BuildInfo buildInfo = await getBuildInfo(); final BuildInfo buildInfo = await getBuildInfo();
final DebuggingOptions debuggingOptions = await createDebuggingOptions(web); final DebuggingOptions debuggingOptions = await createDebuggingOptions(web);
final File applicationBinary = stringArg('use-application-binary') == null final File applicationBinary = applicationBinaryPath == null
? null ? null
: _fileSystem.file(stringArg('use-application-binary')); : _fileSystem.file(applicationBinaryPath);
bool screenshotTaken = false; bool screenshotTaken = false;
try { try {
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
import '../android/android_device.dart'; import '../android/android_device.dart';
import '../application_package.dart'; import '../application_package.dart';
import '../base/common.dart'; import '../base/common.dart';
import '../base/file_system.dart';
import '../base/io.dart'; import '../base/io.dart';
import '../device.dart'; import '../device.dart';
import '../globals.dart' as globals; import '../globals.dart' as globals;
...@@ -15,6 +16,7 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts ...@@ -15,6 +16,7 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts
requiresPubspecYaml(); requiresPubspecYaml();
usesDeviceUserOption(); usesDeviceUserOption();
usesDeviceTimeoutOption(); usesDeviceTimeoutOption();
usesApplicationBinaryOption();
argParser.addFlag('uninstall-only', argParser.addFlag('uninstall-only',
help: 'Uninstall the app if already on the device. Skip install.', help: 'Uninstall the app if already on the device. Skip install.',
); );
...@@ -34,6 +36,9 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts ...@@ -34,6 +36,9 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts
bool get uninstallOnly => boolArg('uninstall-only'); bool get uninstallOnly => boolArg('uninstall-only');
String? get userIdentifier => stringArg(FlutterOptions.kDeviceUser); String? get userIdentifier => stringArg(FlutterOptions.kDeviceUser);
String? get _applicationBinaryPath => stringArg(FlutterOptions.kUseApplicationBinary);
File? get _applicationBinary => _applicationBinaryPath == null ? null : globals.fs.file(_applicationBinaryPath);
@override @override
Future<void> validateCommand() async { Future<void> validateCommand() async {
await super.validateCommand(); await super.validateCommand();
...@@ -44,6 +49,9 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts ...@@ -44,6 +49,9 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts
if (userIdentifier != null && device is! AndroidDevice) { if (userIdentifier != null && device is! AndroidDevice) {
throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android'); throwToolExit('--${FlutterOptions.kDeviceUser} is only supported for Android');
} }
if (_applicationBinaryPath != null && !(_applicationBinary?.existsSync() ?? true)) {
throwToolExit('Prebuilt binary $_applicationBinaryPath does not exist');
}
} }
@override @override
...@@ -51,6 +59,7 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts ...@@ -51,6 +59,7 @@ class InstallCommand extends FlutterCommand with DeviceBasedDevelopmentArtifacts
final Device targetDevice = device!; final Device targetDevice = device!;
final ApplicationPackage? package = await applicationPackages?.getPackageForPlatform( final ApplicationPackage? package = await applicationPackages?.getPackageForPlatform(
await targetDevice.targetPlatform, await targetDevice.targetPlatform,
applicationBinary: _applicationBinary,
); );
if (package == null) { if (package == null) {
throwToolExit('Could not find or build package'); throwToolExit('Could not find or build package');
......
...@@ -38,6 +38,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment ...@@ -38,6 +38,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
usesWebRendererOption(); usesWebRendererOption();
addNativeNullAssertions(hide: !verboseHelp); addNativeNullAssertions(hide: !verboseHelp);
addBundleSkSLPathOption(hide: !verboseHelp); addBundleSkSLPathOption(hide: !verboseHelp);
usesApplicationBinaryOption();
argParser argParser
..addFlag('trace-startup', ..addFlag('trace-startup',
negatable: false, negatable: false,
...@@ -87,12 +88,6 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment ...@@ -87,12 +88,6 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
help: '(deprecated) Allow connections to the VM service without using authentication codes. ' help: '(deprecated) Allow connections to the VM service without using authentication codes. '
'(Not recommended! This can open your device to remote code execution attacks!)' '(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', ..addFlag('start-paused',
defaultsTo: startPausedDefault, defaultsTo: startPausedDefault,
help: 'Start in a paused mode and wait for a debugger to connect.', help: 'Start in a paused mode and wait for a debugger to connect.',
...@@ -168,7 +163,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment ...@@ -168,7 +163,7 @@ abstract class RunCommandBase extends FlutterCommand with DeviceBasedDevelopment
bool get purgePersistentCache => boolArg('purge-persistent-cache'); bool get purgePersistentCache => boolArg('purge-persistent-cache');
bool get disableServiceAuthCodes => boolArg('disable-service-auth-codes'); bool get disableServiceAuthCodes => boolArg('disable-service-auth-codes');
bool get cacheStartupProfile => boolArg('cache-startup-profile'); 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 trackWidgetCreation => boolArg('track-widget-creation');
bool get enableImpeller => boolArg('enable-impeller'); bool get enableImpeller => boolArg('enable-impeller');
...@@ -347,7 +342,7 @@ class RunCommand extends RunCommandBase { ...@@ -347,7 +342,7 @@ class RunCommand extends RunCommandBase {
defaultsTo: false, defaultsTo: false,
help: 'Whether to quickly bootstrap applications with a minimal app. ' help: 'Whether to quickly bootstrap applications with a minimal app. '
'Currently this is only supported on Android devices. This option ' '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, hide: !verboseHelp,
); );
} }
...@@ -484,7 +479,7 @@ class RunCommand extends RunCommandBase { ...@@ -484,7 +479,7 @@ class RunCommand extends RunCommandBase {
throwToolExit(null); throwToolExit(null);
} }
if (globals.deviceManager.hasSpecifiedAllDevices && runningWithPrebuiltApplication) { 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 if (userIdentifier != null
...@@ -562,7 +557,7 @@ class RunCommand extends RunCommandBase { ...@@ -562,7 +557,7 @@ class RunCommand extends RunCommandBase {
// debug mode. // debug mode.
final BuildInfo buildInfo = await getBuildInfo(); final BuildInfo buildInfo = await getBuildInfo();
final bool hotMode = shouldUseHotMode(buildInfo); final bool hotMode = shouldUseHotMode(buildInfo);
final String applicationBinaryPath = stringArg('use-application-binary'); final String applicationBinaryPath = stringArg(FlutterOptions.kUseApplicationBinary);
if (boolArg('machine')) { if (boolArg('machine')) {
if (devices.length > 1) { if (devices.length > 1) {
......
...@@ -117,6 +117,7 @@ class FlutterOptions { ...@@ -117,6 +117,7 @@ class FlutterOptions {
static const String kInitializeFromDill = 'initialize-from-dill'; static const String kInitializeFromDill = 'initialize-from-dill';
static const String kAssumeInitializeFromDillUpToDate = 'assume-initialize-from-dill-up-to-date'; static const String kAssumeInitializeFromDillUpToDate = 'assume-initialize-from-dill-up-to-date';
static const String kFatalWarnings = 'fatal-warnings'; static const String kFatalWarnings = 'fatal-warnings';
static const String kUseApplicationBinary = 'use-application-binary';
} }
/// flutter command categories for usage. /// flutter command categories for usage.
...@@ -606,6 +607,16 @@ abstract class FlutterCommand extends Command<void> { ...@@ -606,6 +607,16 @@ abstract class FlutterCommand extends Command<void> {
); );
} }
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. /// Whether it is safe for this command to use a cached pub invocation.
bool get cachePubGet => true; bool get cachePubGet => true;
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
// @dart = 2.8 // @dart = 2.8
import 'package:file/file.dart'; 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/android_device.dart';
import 'package:flutter_tools/src/android/application_package.dart'; import 'package:flutter_tools/src/android/application_package.dart';
import 'package:flutter_tools/src/application_package.dart'; import 'package:flutter_tools/src/application_package.dart';
...@@ -18,6 +19,7 @@ import 'package:test/fake.dart'; ...@@ -18,6 +19,7 @@ import 'package:test/fake.dart';
import '../../src/common.dart'; import '../../src/common.dart';
import '../../src/context.dart'; import '../../src/context.dart';
import '../../src/fake_process_manager.dart';
import '../../src/test_flutter_command_runner.dart'; import '../../src/test_flutter_command_runner.dart';
void main() { void main() {
...@@ -26,6 +28,12 @@ void main() { ...@@ -26,6 +28,12 @@ void main() {
Cache.disableLocking(); 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 { testUsingContext('returns 0 when Android is connected and ready for an install', () async {
final InstallCommand command = InstallCommand(); final InstallCommand command = InstallCommand();
command.applicationPackages = FakeApplicationPackageFactory(FakeAndroidApk()); command.applicationPackages = FakeApplicationPackageFactory(FakeAndroidApk());
...@@ -36,6 +44,8 @@ void main() { ...@@ -36,6 +44,8 @@ void main() {
await createTestCommandRunner(command).run(<String>['install']); await createTestCommandRunner(command).run(<String>['install']);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()), Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('returns 1 when targeted device is not Android with --device-user', () async { testUsingContext('returns 1 when targeted device is not Android with --device-user', () async {
...@@ -49,6 +59,8 @@ void main() { ...@@ -49,6 +59,8 @@ void main() {
throwsToolExit(message: '--device-user is only supported for Android')); throwsToolExit(message: '--device-user is only supported for Android'));
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()), Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
}); });
testUsingContext('returns 0 when iOS is connected and ready for an install', () async { testUsingContext('returns 0 when iOS is connected and ready for an install', () async {
...@@ -61,6 +73,38 @@ void main() { ...@@ -61,6 +73,38 @@ void main() {
await createTestCommandRunner(command).run(<String>['install']); await createTestCommandRunner(command).run(<String>['install']);
}, overrides: <Type, Generator>{ }, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()), 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(<String>['install', '--use-application-binary', 'bogus']),
throwsToolExit(message: 'Prebuilt binary bogus does not exist'));
}, overrides: <Type, Generator>{
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(<String>['install', '--use-application-binary', 'binary']);
}, overrides: <Type, Generator>{
Cache: () => Cache.test(processManager: FakeProcessManager.any()),
FileSystem: () => fileSystem,
ProcessManager: () => FakeProcessManager.any(),
}); });
}); });
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment