Commit 18212382 authored by Dan Rubel's avatar Dan Rubel Committed by GitHub

Refactor flutter command execution (#5892)

* refactor _run to runCmd

* replace requiresProjectRoot getter with call to commandValidator

* replace requiresDevice getter with call to findTargetDevice

* trace command requires a debug connection, not a device

* inline androidOnly getter

* rename command methods to verifyTheRunCmd and runCmd

* move common verification into BuildSubCommand

* rename deviceForCommand to device

* rename methods to verifyThenRunCommand and runCommand
parent 0873f3e1
......@@ -60,10 +60,7 @@ class AnalyzeCommand extends FlutterCommand {
}
@override
bool get requiresProjectRoot => false;
@override
Future<int> runInProject() => argResults['watch'] ? _analyzeWatch() : _analyzeOnce();
Future<int> runCommand() => argResults['watch'] ? _analyzeWatch() : _analyzeOnce();
List<String> flutterRootComponents;
bool isFlutterLibrary(String filename) {
......
......@@ -32,13 +32,28 @@ class BuildCommand extends FlutterCommand {
final String description = 'Flutter build commands.';
@override
Future<int> runInProject() => new Future<int>.value(0);
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runCommand() => new Future<int>.value(0);
}
abstract class BuildSubCommand extends FlutterCommand {
@override
@mustCallSuper
Future<int> runInProject() async {
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
return super.verifyThenRunCommand();
}
@override
@mustCallSuper
Future<int> runCommand() async {
if (isRunningOnBot) {
File dotPackages = new File('.packages');
printStatus('Contents of .packages:');
......@@ -66,7 +81,14 @@ class BuildCleanCommand extends FlutterCommand {
final String description = 'Delete the build/ directory.';
@override
Future<int> runInProject() async {
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runCommand() async {
Directory buildDir = new Directory(getBuildDirectory());
printStatus("Deleting '${buildDir.path}${Platform.pathSeparator}'.");
......
......@@ -42,8 +42,8 @@ class BuildAotCommand extends BuildSubCommand {
final String description = "Build an ahead-of-time compiled snapshot of your app's Dart code.";
@override
Future<int> runInProject() async {
await super.runInProject();
Future<int> runCommand() async {
await super.runCommand();
String targetPlatform = argResults['target-platform'];
TargetPlatform platform = getTargetPlatformForName(targetPlatform);
if (platform == null) {
......
......@@ -221,8 +221,8 @@ class BuildApkCommand extends BuildSubCommand {
}
@override
Future<int> runInProject() async {
await super.runInProject();
Future<int> runCommand() async {
await super.runCommand();
TargetPlatform targetPlatform = _getTargetPlatform(argResults['target-arch']);
if (targetPlatform != TargetPlatform.android_arm && getBuildMode() != BuildMode.debug) {
......
......@@ -38,8 +38,8 @@ class BuildFlxCommand extends BuildSubCommand {
'they are used by some Flutter Android and iOS runtimes.';
@override
Future<int> runInProject() async {
await super.runInProject();
Future<int> runCommand() async {
await super.runCommand();
String outputPath = argResults['output-file'];
return await build(
......
......@@ -38,11 +38,11 @@ class BuildIOSCommand extends BuildSubCommand {
final String description = 'Build an iOS application bundle (Mac OS X host only).';
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
bool forSimulator = argResults['simulator'];
defaultBuildMode = forSimulator ? BuildMode.debug : BuildMode.release;
await super.runInProject();
await super.runCommand();
if (getCurrentHostPlatform() != HostPlatform.darwin_x64) {
printError('Building for iOS is only supported on the Mac.');
return 1;
......
......@@ -20,10 +20,7 @@ class ChannelCommand extends FlutterCommand {
String get invocation => '${runner.executableName} $name [<channel-name>]';
@override
bool get requiresProjectRoot => false;
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
switch (argResults.rest.length) {
case 0:
return await _listChannels();
......
......@@ -41,15 +41,12 @@ class ConfigCommand extends FlutterCommand {
'Analytics reporting is currently ${flutterUsage.enabled ? 'enabled' : 'disabled'}.';
}
@override
bool get requiresProjectRoot => false;
/// Return `null` to disable tracking of the `config` command.
@override
String get usagePath => null;
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
if (argResults.wasParsed('analytics')) {
bool value = argResults['analytics'];
flutterUsage.enabled = value;
......
......@@ -41,14 +41,11 @@ class CreateCommand extends FlutterCommand {
final String description = 'Create a new Flutter project.\n\n'
'If run on a project that already exists, this will repair the project, recreating any files that are missing.';
@override
bool get requiresProjectRoot => false;
@override
String get invocation => "${runner.executableName} $name <output directory>";
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
if (argResults.rest.isEmpty) {
printStatus('No option specified for the output directory.');
printStatus(usage);
......
......@@ -37,14 +37,11 @@ class DaemonCommand extends FlutterCommand {
@override
final String description = 'Run a persistent, JSON-RPC based server to communicate with devices.';
@override
bool get requiresProjectRoot => false;
@override
final bool hidden;
@override
Future<int> runInProject() {
Future<int> runCommand() {
printStatus('Starting device daemon...');
AppContext appContext = new AppContext();
......
......@@ -17,10 +17,7 @@ class DevicesCommand extends FlutterCommand {
final String description = 'List all connected devices.';
@override
bool get requiresProjectRoot => false;
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
if (!doctor.canListAnything) {
printError("Unable to locate a development device; please run 'flutter doctor' for "
"information about installing additional components.");
......
......@@ -15,10 +15,7 @@ class DoctorCommand extends FlutterCommand {
final String description = 'Show information about the installed tooling.';
@override
bool get requiresProjectRoot => false;
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
await doctor.diagnose();
return 0;
}
......
......@@ -86,7 +86,14 @@ class DriveCommand extends RunCommandBase {
int get debugPort => int.parse(argResults['debug-port']);
@override
Future<int> runInProject() async {
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runCommand() async {
String testFile = _getTestFile();
if (testFile == null) {
return 1;
......
......@@ -21,14 +21,11 @@ class FormatCommand extends FlutterCommand {
@override
final String description = 'Format one or more dart files.';
@override
bool get requiresProjectRoot => false;
@override
String get invocation => "${runner.executableName} $name <one or more paths>";
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
if (argResults.rest.isEmpty) {
printStatus('No files specified to be formatted.');
printStatus('');
......
......@@ -17,12 +17,20 @@ class InstallCommand extends FlutterCommand {
@override
final String description = 'Install a Flutter app on an attached device.';
Device device;
@override
bool get requiresDevice => true;
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
device = await findTargetDevice();
if (device == null)
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runInProject() async {
Device device = deviceForCommand;
Future<int> runCommand() async {
ApplicationPackage package = applicationPackages.getPackageForPlatform(device.platform);
Cache.releaseLockEarly();
......
......@@ -29,11 +29,20 @@ class ListenCommand extends RunCommandBase {
/// Only run once. Used for testing.
final bool singleRun;
Device device;
@override
bool get requiresDevice => true;
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
device = await findTargetDevice();
if (device == null)
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
Iterable<String> directories = () sync* {
yield* argResults.rest;
yield '.';
......@@ -61,7 +70,7 @@ class ListenCommand extends RunCommandBase {
printStatus('Re-running app...');
result = await startApp(
deviceForCommand,
device,
target: targetFile,
install: firstTime,
stop: true,
......
......@@ -25,16 +25,18 @@ class LogsCommand extends FlutterCommand {
@override
final String description = 'Show log output for running Flutter apps.';
@override
bool get requiresProjectRoot => false;
Device device;
@override
bool get requiresDevice => true;
Future<int> verifyThenRunCommand() async {
device = await findTargetDevice();
if (device == null)
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runInProject() async {
Device device = deviceForCommand;
Future<int> runCommand() async {
if (argResults['clear'])
device.clearLogs();
......
......@@ -25,7 +25,14 @@ class PackagesCommand extends FlutterCommand {
final String description = 'Commands for managing Flutter packages.';
@override
Future<int> runInProject() => new Future<int>.value(0);
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runCommand() => new Future<int>.value(0);
}
class PackagesGetCommand extends FlutterCommand {
......@@ -41,15 +48,12 @@ class PackagesGetCommand extends FlutterCommand {
String get description =>
(upgrade ? 'Upgrade' : 'Get') + ' packages in a Flutter project.';
@override
bool get requiresProjectRoot => false;
@override
String get invocation =>
"${runner.executableName} packages $name [<target directory>]";
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
if (argResults.rest.length > 1) {
printStatus('Too many arguments.');
printStatus(usage);
......
......@@ -20,10 +20,7 @@ class PrecacheCommand extends FlutterCommand {
final String description = 'Populates the Flutter tool\'s cache of binary artifacts.';
@override
bool get requiresProjectRoot => false;
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
if (argResults['all-platforms'])
cache.includeAllPlatforms = true;
......
......@@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter_tools/src/device.dart';
import 'package:path/path.dart' as path;
import '../android/android_device.dart';
......@@ -29,14 +30,20 @@ class RefreshCommand extends FlutterCommand {
);
}
@override
bool get androidOnly => true;
Device device;
@override
bool get requiresDevice => true;
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
device = await findTargetDevice(androidOnly: true);
if (device == null)
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
Directory tempDir = await Directory.systemTemp.createTemp('flutter_tools');
try {
String snapshotPath = path.join(tempDir.path, 'snapshot_blob.bin');
......@@ -49,8 +56,6 @@ class RefreshCommand extends FlutterCommand {
Cache.releaseLockEarly();
AndroidDevice device = deviceForCommand;
String activity = argResults['activity'];
if (activity == null) {
AndroidApk apk = applicationPackages.getPackageForPlatform(device.platform);
......@@ -62,7 +67,9 @@ class RefreshCommand extends FlutterCommand {
}
}
bool success = await device.refreshSnapshot(activity, snapshotPath);
AndroidDevice androidDevice = device;
bool success = await androidDevice.refreshSnapshot(activity, snapshotPath);
if (!success) {
printError('Error refreshing snapshot on $device.');
return 1;
......
......@@ -83,13 +83,10 @@ class RunCommand extends RunCommandBase {
argParser.addFlag('benchmark', negatable: false, hide: true);
}
@override
bool get requiresDevice => true;
Device device;
@override
String get usagePath {
Device device = deviceForCommand;
String command = shouldUseHotMode() ? 'hotrun' : name;
if (device == null)
......@@ -116,7 +113,17 @@ class RunCommand extends RunCommandBase {
}
@override
Future<int> runInProject() async {
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
device = await findTargetDevice();
if (device == null)
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runCommand() async {
int debugPort;
if (argResults['debug-port'] != null) {
......@@ -128,7 +135,7 @@ class RunCommand extends RunCommandBase {
}
}
if (deviceForCommand.isLocalEmulator && !isEmulatorBuildMode(getBuildMode())) {
if (device.isLocalEmulator && !isEmulatorBuildMode(getBuildMode())) {
printError('${toTitleCase(getModeName(getBuildMode()))} mode is not supported for emulators.');
return 1;
}
......@@ -152,7 +159,7 @@ class RunCommand extends RunCommandBase {
final bool hotMode = shouldUseHotMode();
if (hotMode) {
if (!deviceForCommand.supportsHotMode) {
if (!device.supportsHotMode) {
printError('Hot mode is not supported by this device. '
'Run with --no-hot.');
return 1;
......@@ -168,14 +175,14 @@ class RunCommand extends RunCommandBase {
if (hotMode) {
runner = new HotRunner(
deviceForCommand,
device,
target: targetFile,
debuggingOptions: options,
benchmarkMode: argResults['benchmark'],
);
} else {
runner = new RunAndStayResident(
deviceForCommand,
device,
target: targetFile,
debuggingOptions: options,
traceStartup: traceStartup,
......
......@@ -42,9 +42,6 @@ class RunMojoCommand extends FlutterCommand {
@override
final bool hidden;
@override
bool get requiresProjectRoot => false;
// TODO(abarth): Why not use path.absolute?
String _makePathAbsolute(String relativePath) {
File file = new File(relativePath);
......@@ -127,7 +124,7 @@ class RunMojoCommand extends FlutterCommand {
}
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
if ((argResults['mojo-path'] == null && argResults['devtools-path'] == null) || (argResults['mojo-path'] != null && argResults['devtools-path'] != null)) {
printError('Must specify either --mojo-path or --devtools-path.');
return 1;
......
......@@ -5,6 +5,7 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter_tools/src/device.dart';
import 'package:path/path.dart' as path;
import '../base/utils.dart';
......@@ -27,16 +28,20 @@ class ScreenshotCommand extends FlutterCommand {
@override
final List<String> aliases = <String>['pic'];
@override
bool get requiresProjectRoot => false;
Device device;
@override
bool get requiresDevice => true;
Future<int> verifyThenRunCommand() async {
device = await findTargetDevice();
if (device == null)
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runInProject() async {
if (!deviceForCommand.supportsScreenshot) {
printError('Screenshot not supported for ${deviceForCommand.name}.');
Future<int> runCommand() async {
if (!device.supportsScreenshot) {
printError('Screenshot not supported for ${device.name}.');
return 1;
}
......@@ -49,7 +54,7 @@ class ScreenshotCommand extends FlutterCommand {
}
try {
bool result = await deviceForCommand.takeScreenshot(outputFile);
bool result = await device.takeScreenshot(outputFile);
if (result) {
int sizeKB = outputFile.lengthSync() ~/ 1000;
......
......@@ -31,10 +31,7 @@ class SetupCommand extends FlutterCommand {
final bool hidden;
@override
bool get requiresProjectRoot => false;
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
printStatus('Running Flutter setup...');
// setup brew on mac
......
......@@ -27,7 +27,14 @@ class SkiaCommand extends FlutterCommand {
final String description = 'Retrieve the last frame rendered by a Flutter app as a Skia picture.';
@override
Future<int> runInProject() async {
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runCommand() async {
File outputFile;
Uri skiaserveUri;
if (argResults['output-file'] != null) {
......
......@@ -17,12 +17,20 @@ class StopCommand extends FlutterCommand {
@override
final String description = 'Stop your Flutter app on an attached device.';
Device device;
@override
bool get requiresDevice => true;
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
device = await findTargetDevice();
if (device == null)
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runInProject() async {
Device device = deviceForCommand;
Future<int> runCommand() async {
ApplicationPackage app = applicationPackages.getPackageForPlatform(device.platform);
if (app == null) {
String platformName = getNameForTargetPlatform(device.platform);
......
......@@ -44,9 +44,6 @@ class TestCommand extends FlutterCommand {
@override
String get description => 'Run Flutter unit tests for the current project.';
@override
bool get requiresProjectRoot => false;
@override
Validator commandValidator = () {
if (!FileSystemEntity.isFileSync('pubspec.yaml')) {
......@@ -147,7 +144,7 @@ class TestCommand extends FlutterCommand {
}
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
List<String> testArgs = argResults.rest.map((String testPath) => path.absolute(testPath)).toList();
if (!commandValidator())
......
......@@ -46,10 +46,14 @@ class TraceCommand extends FlutterCommand {
'with --start and later with --stop.';
@override
bool get requiresDevice => true;
Future<int> verifyThenRunCommand() async {
if (!commandValidator())
return 1;
return super.verifyThenRunCommand();
}
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
int observatoryPort = int.parse(argResults['debug-port']);
Tracing tracing;
......
......@@ -32,9 +32,6 @@ class UpdatePackagesCommand extends FlutterCommand {
@override
final bool hidden;
@override
bool get requiresProjectRoot => false;
Future<Null> _downloadCoverageData() async {
Status status = logger.startProgress("Downloading lcov data for package:flutter...");
final List<int> data = await fetchUrl(Uri.parse('https://storage.googleapis.com/flutter_infra/flutter/coverage/lcov.info'));
......@@ -49,7 +46,7 @@ class UpdatePackagesCommand extends FlutterCommand {
}
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
try {
final Stopwatch timer = new Stopwatch()..start();
int count = 0;
......
......@@ -21,10 +21,7 @@ class UpgradeCommand extends FlutterCommand {
final String description = 'Upgrade your copy of Flutter.';
@override
bool get requiresProjectRoot => false;
@override
Future<int> runInProject() async {
Future<int> runCommand() async {
try {
runCheckedSync(<String>[
'git', 'rev-parse', '@{u}'
......
......@@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:io';
import 'package:args/command_runner.dart';
import 'package:meta/meta.dart';
import '../application_package.dart';
import '../build_info.dart';
......@@ -27,15 +28,6 @@ abstract class FlutterCommand extends Command {
@override
FlutterCommandRunner get runner => super.runner;
/// Whether this command needs to be run from the root of a project.
bool get requiresProjectRoot => true;
/// Whether this command requires a (single) Flutter target device to be connected.
bool get requiresDevice => false;
/// Whether this command only applies to Android devices.
bool get androidOnly => false;
/// Whether this command uses the 'target' option.
bool _usesTargetOption = false;
......@@ -100,7 +92,7 @@ abstract class FlutterCommand extends Command {
return _defaultBuildMode;
}
void _setupApplicationPackages() {
void setupApplicationPackages() {
applicationPackages ??= new ApplicationPackageStore();
}
......@@ -108,12 +100,21 @@ abstract class FlutterCommand extends Command {
/// tracking of the command.
String get usagePath => name;
/// Runs this command.
///
/// Rather than overriding this method, subclasses should override
/// [verifyThenRunCommand] to perform any verification
/// and [runCommand] to execute the command
/// so that this method can record and report the overall time to analytics.
@override
Future<int> run() {
Stopwatch stopwatch = new Stopwatch()..start();
UsageTimer analyticsTimer = usagePath == null ? null : flutterUsage.startTimer(name);
return _run().then((int exitCode) {
if (flutterUsage.isFirstRun)
flutterUsage.printUsage();
return verifyThenRunCommand().then((int exitCode) {
int ms = stopwatch.elapsedMilliseconds;
printTrace("'flutter $name' took ${ms}ms; exiting with code $exitCode.");
analyticsTimer?.finish();
......@@ -121,55 +122,15 @@ abstract class FlutterCommand extends Command {
});
}
Future<int> _run() async {
if (requiresProjectRoot && !commandValidator())
return 1;
// Ensure at least one toolchain is installed.
if (requiresDevice && !doctor.canLaunchAnything) {
printError("Unable to locate a development device; please run 'flutter doctor' "
"for information about installing additional components.");
return 1;
}
// Validate devices.
if (requiresDevice) {
List<Device> devices = await deviceManager.getDevices();
if (devices.isEmpty && deviceManager.hasSpecifiedDeviceId) {
printStatus("No devices found with name or id "
"matching '${deviceManager.specifiedDeviceId}'");
return 1;
} else if (devices.isEmpty) {
printNoConnectedDevices();
return 1;
}
devices = devices.where((Device device) => device.isSupported()).toList();
if (androidOnly)
devices = devices.where((Device device) => device.platform == TargetPlatform.android_arm).toList();
if (devices.isEmpty) {
printStatus('No supported devices connected.');
return 1;
} else if (devices.length > 1) {
if (deviceManager.hasSpecifiedDeviceId) {
printStatus("Found ${devices.length} devices with name or id matching "
"'${deviceManager.specifiedDeviceId}':");
} else {
printStatus("More than one device connected; please specify a device with "
"the '-d <deviceId>' flag.");
devices = await deviceManager.getAllConnectedDevices();
}
printStatus('');
Device.printDevices(devices);
return 1;
} else {
_deviceForCommand = devices.single;
}
}
/// Perform validation then call [runCommand] to execute the command.
/// Return a [Future] that completes with an exit code
/// indicating whether execution was successful.
///
/// Subclasses should override this method to perform verification
/// then call this method to execute the command
/// rather than calling [runCommand] directly.
@mustCallSuper
Future<int> verifyThenRunCommand() async {
// Populate the cache. We call this before pub get below so that the sky_engine
// package is available in the flutter cache for pub to find.
await cache.updateAll();
......@@ -180,16 +141,62 @@ abstract class FlutterCommand extends Command {
return exitCode;
}
if (flutterUsage.isFirstRun)
flutterUsage.printUsage();
_setupApplicationPackages();
setupApplicationPackages();
String commandPath = usagePath;
if (commandPath != null)
flutterUsage.sendCommand(usagePath);
return await runInProject();
return await runCommand();
}
/// Subclasses must implement this to execute the command.
Future<int> runCommand();
/// Find and return the target [Device] based upon currently connected
/// devices and criteria entered by the user on the command line.
/// If a device cannot be found that meets specified criteria,
/// then print an error message and return `null`.
Future<Device> findTargetDevice({bool androidOnly: false}) async {
if (!doctor.canLaunchAnything) {
printError("Unable to locate a development device; please run 'flutter doctor' "
"for information about installing additional components.");
return null;
}
List<Device> devices = await deviceManager.getDevices();
if (devices.isEmpty && deviceManager.hasSpecifiedDeviceId) {
printStatus("No devices found with name or id "
"matching '${deviceManager.specifiedDeviceId}'");
return null;
} else if (devices.isEmpty) {
printNoConnectedDevices();
return null;
}
devices = devices.where((Device device) => device.isSupported()).toList();
if (androidOnly)
devices = devices.where((Device device) => device.platform == TargetPlatform.android_arm).toList();
if (devices.isEmpty) {
printStatus('No supported devices connected.');
return null;
} else if (devices.length > 1) {
if (deviceManager.hasSpecifiedDeviceId) {
printStatus("Found ${devices.length} devices with name or id matching "
"'${deviceManager.specifiedDeviceId}':");
} else {
printStatus("More than one device connected; please specify a device with "
"the '-d <deviceId>' flag.");
devices = await deviceManager.getAllConnectedDevices();
}
printStatus('');
Device.printDevices(devices);
return null;
}
return devices.single;
}
void printNoConnectedDevices() {
......@@ -230,12 +237,5 @@ abstract class FlutterCommand extends Command {
return true;
}
Future<int> runInProject();
// This is calculated in run() if the command has [requiresDevice] specified.
Device _deviceForCommand;
Device get deviceForCommand => _deviceForCommand;
ApplicationPackageStore applicationPackages;
}
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