mirror of
https://github.com/flutter/flutter.git
synced 2025-06-03 00:51:18 +00:00
New tooling for iOS 17 physical devices (#131865)
This PR includes the following changes. These changes only apply to iOS 17 physical devices. | Command | Change Description | Changes to User Experience | | ------------- | ------------- | ------------- | | `flutter run --release` | Uses `devicectl` to install and launch application in release mode. | No change. | | `flutter run` | Uses Xcode via automation scripting to run application in debug and profile mode. | Xcode will be opened in the background. Errors/crashes may be caught in Xcode and therefore may not show in terminal. | | `flutter run --use-application-binary=xxxx` | Creates temporary empty Xcode project and use Xcode to run via automation scripting in debug and profile. | Xcode will be opened in the background. Errors/crashes may be caught in Xcode and therefore may not show in terminal. | | `flutter install` | Uses `devicectl` to check installed apps, install app, uninstall app. | No change. | | `flutter screenshot` | Will return error. | Will return error. | Other changes include: * Using `devicectl` to get information about the device * Using `idevicesyslog` and Dart VM logging for device logs Note: Xcode automation scripting (used in `flutter run` for debug and profile) does not work in a headless (without a UI) interface. No known workaround. Fixes https://github.com/flutter/flutter/issues/128827, https://github.com/flutter/flutter/issues/128531.
This commit is contained in:
parent
88ed9bd9d2
commit
d631b26285
20
.ci.yaml
20
.ci.yaml
@ -3685,6 +3685,16 @@ targets:
|
|||||||
["devicelab", "ios", "mac"]
|
["devicelab", "ios", "mac"]
|
||||||
task_name: flutter_gallery_ios__start_up
|
task_name: flutter_gallery_ios__start_up
|
||||||
|
|
||||||
|
- name: Mac_ios flutter_gallery_ios__start_up_xcode_debug
|
||||||
|
recipe: devicelab/devicelab_drone
|
||||||
|
presubmit: false
|
||||||
|
timeout: 60
|
||||||
|
properties:
|
||||||
|
tags: >
|
||||||
|
["devicelab", "ios", "mac"]
|
||||||
|
task_name: flutter_gallery_ios__start_up_xcode_debug
|
||||||
|
bringup: true
|
||||||
|
|
||||||
- name: Mac_ios flutter_view_ios__start_up
|
- name: Mac_ios flutter_view_ios__start_up
|
||||||
recipe: devicelab/devicelab_drone
|
recipe: devicelab/devicelab_drone
|
||||||
presubmit: false
|
presubmit: false
|
||||||
@ -3752,6 +3762,16 @@ targets:
|
|||||||
["devicelab", "ios", "mac"]
|
["devicelab", "ios", "mac"]
|
||||||
task_name: integration_ui_ios_driver
|
task_name: integration_ui_ios_driver
|
||||||
|
|
||||||
|
- name: Mac_ios integration_ui_ios_driver_xcode_debug
|
||||||
|
recipe: devicelab/devicelab_drone
|
||||||
|
presubmit: false
|
||||||
|
timeout: 60
|
||||||
|
properties:
|
||||||
|
tags: >
|
||||||
|
["devicelab", "ios", "mac"]
|
||||||
|
task_name: integration_ui_ios_driver_xcode_debug
|
||||||
|
bringup: true
|
||||||
|
|
||||||
- name: Mac_ios integration_ui_ios_frame_number
|
- name: Mac_ios integration_ui_ios_frame_number
|
||||||
recipe: devicelab/devicelab_drone
|
recipe: devicelab/devicelab_drone
|
||||||
presubmit: false
|
presubmit: false
|
||||||
|
@ -168,6 +168,7 @@
|
|||||||
/dev/devicelab/bin/tasks/flutter_gallery__transition_perf_e2e_ios.dart @zanderso @flutter/engine
|
/dev/devicelab/bin/tasks/flutter_gallery__transition_perf_e2e_ios.dart @zanderso @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/flutter_gallery_ios__compile.dart @vashworth @flutter/engine
|
/dev/devicelab/bin/tasks/flutter_gallery_ios__compile.dart @vashworth @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/flutter_gallery_ios__start_up.dart @vashworth @flutter/engine
|
/dev/devicelab/bin/tasks/flutter_gallery_ios__start_up.dart @vashworth @flutter/engine
|
||||||
|
/dev/devicelab/bin/tasks/flutter_gallery_ios__start_up_xcode_debug.dart @vashworth @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/flutter_gallery_ios_sksl_warmup__transition_perf.dart @zanderso @flutter/engine
|
/dev/devicelab/bin/tasks/flutter_gallery_ios_sksl_warmup__transition_perf.dart @zanderso @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/flutter_view_ios__start_up.dart @zanderso @flutter/engine
|
/dev/devicelab/bin/tasks/flutter_view_ios__start_up.dart @zanderso @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/fullscreen_textfield_perf_ios__e2e_summary.dart @cyanglaz @flutter/engine
|
/dev/devicelab/bin/tasks/fullscreen_textfield_perf_ios__e2e_summary.dart @cyanglaz @flutter/engine
|
||||||
@ -178,6 +179,7 @@
|
|||||||
/dev/devicelab/bin/tasks/imagefiltered_transform_animation_perf_ios__timeline_summary.dart @cyanglaz @flutter/engine
|
/dev/devicelab/bin/tasks/imagefiltered_transform_animation_perf_ios__timeline_summary.dart @cyanglaz @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/integration_test_test_ios.dart @cyanglaz @flutter/engine
|
/dev/devicelab/bin/tasks/integration_test_test_ios.dart @cyanglaz @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/integration_ui_ios_driver.dart @cyanglaz @flutter/tool
|
/dev/devicelab/bin/tasks/integration_ui_ios_driver.dart @cyanglaz @flutter/tool
|
||||||
|
/dev/devicelab/bin/tasks/integration_ui_ios_driver_xcode_debug.dart @vashworth @flutter/tool
|
||||||
/dev/devicelab/bin/tasks/integration_ui_ios_frame_number.dart @iskakaushik @flutter/engine
|
/dev/devicelab/bin/tasks/integration_ui_ios_frame_number.dart @iskakaushik @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/integration_ui_ios_keyboard_resize.dart @cyanglaz @flutter/engine
|
/dev/devicelab/bin/tasks/integration_ui_ios_keyboard_resize.dart @cyanglaz @flutter/engine
|
||||||
/dev/devicelab/bin/tasks/integration_ui_ios_screenshot.dart @cyanglaz @flutter/tool
|
/dev/devicelab/bin/tasks/integration_ui_ios_screenshot.dart @cyanglaz @flutter/tool
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
// 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 'package:flutter_devicelab/framework/devices.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
import 'package:flutter_devicelab/tasks/perf_tests.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
// TODO(vashworth): Remove after Xcode 15 and iOS 17 are in CI (https://github.com/flutter/flutter/issues/132128)
|
||||||
|
// XcodeDebug workflow is used for CoreDevices (iOS 17+ and Xcode 15+). Use
|
||||||
|
// FORCE_XCODE_DEBUG environment variable to force the use of XcodeDebug
|
||||||
|
// workflow in CI to test from older versions since devicelab has not yet been
|
||||||
|
// updated to iOS 17 and Xcode 15.
|
||||||
|
deviceOperatingSystem = DeviceOperatingSystem.ios;
|
||||||
|
await task(createFlutterGalleryStartupTest(
|
||||||
|
runEnvironment: <String, String>{
|
||||||
|
'FORCE_XCODE_DEBUG': 'true',
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
// 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 'package:flutter_devicelab/framework/devices.dart';
|
||||||
|
import 'package:flutter_devicelab/framework/framework.dart';
|
||||||
|
import 'package:flutter_devicelab/tasks/integration_tests.dart';
|
||||||
|
|
||||||
|
Future<void> main() async {
|
||||||
|
// TODO(vashworth): Remove after Xcode 15 and iOS 17 are in CI (https://github.com/flutter/flutter/issues/132128)
|
||||||
|
// XcodeDebug workflow is used for CoreDevices (iOS 17+ and Xcode 15+). Use
|
||||||
|
// FORCE_XCODE_DEBUG environment variable to force the use of XcodeDebug
|
||||||
|
// workflow in CI to test from older versions since devicelab has not yet been
|
||||||
|
// updated to iOS 17 and Xcode 15.
|
||||||
|
deviceOperatingSystem = DeviceOperatingSystem.ios;
|
||||||
|
await task(createEndToEndDriverTest(
|
||||||
|
environment: <String, String>{
|
||||||
|
'FORCE_XCODE_DEBUG': 'true',
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
@ -106,10 +106,11 @@ TaskFunction createEndToEndFrameNumberTest() {
|
|||||||
).call;
|
).call;
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskFunction createEndToEndDriverTest() {
|
TaskFunction createEndToEndDriverTest({Map<String, String>? environment}) {
|
||||||
return DriverTest(
|
return DriverTest(
|
||||||
'${flutterDirectory.path}/dev/integration_tests/ui',
|
'${flutterDirectory.path}/dev/integration_tests/ui',
|
||||||
'lib/driver.dart',
|
'lib/driver.dart',
|
||||||
|
environment: environment,
|
||||||
).call;
|
).call;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +174,7 @@ class DriverTest {
|
|||||||
this.testTarget, {
|
this.testTarget, {
|
||||||
this.extraOptions = const <String>[],
|
this.extraOptions = const <String>[],
|
||||||
this.deviceIdOverride,
|
this.deviceIdOverride,
|
||||||
|
this.environment,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -180,6 +182,7 @@ class DriverTest {
|
|||||||
final String testTarget;
|
final String testTarget;
|
||||||
final List<String> extraOptions;
|
final List<String> extraOptions;
|
||||||
final String? deviceIdOverride;
|
final String? deviceIdOverride;
|
||||||
|
final Map<String, String>? environment;
|
||||||
|
|
||||||
Future<TaskResult> call() {
|
Future<TaskResult> call() {
|
||||||
return inDirectory<TaskResult>(testDirectory, () async {
|
return inDirectory<TaskResult>(testDirectory, () async {
|
||||||
@ -202,7 +205,7 @@ class DriverTest {
|
|||||||
deviceId,
|
deviceId,
|
||||||
...extraOptions,
|
...extraOptions,
|
||||||
];
|
];
|
||||||
await flutter('drive', options: options);
|
await flutter('drive', options: options, environment: environment);
|
||||||
|
|
||||||
return TaskResult.success(null);
|
return TaskResult.success(null);
|
||||||
});
|
});
|
||||||
|
@ -233,10 +233,11 @@ TaskFunction createOpenPayScrollPerfTest({bool measureCpuGpu = true}) {
|
|||||||
).run;
|
).run;
|
||||||
}
|
}
|
||||||
|
|
||||||
TaskFunction createFlutterGalleryStartupTest({String target = 'lib/main.dart'}) {
|
TaskFunction createFlutterGalleryStartupTest({String target = 'lib/main.dart', Map<String, String>? runEnvironment}) {
|
||||||
return StartupTest(
|
return StartupTest(
|
||||||
'${flutterDirectory.path}/dev/integration_tests/flutter_gallery',
|
'${flutterDirectory.path}/dev/integration_tests/flutter_gallery',
|
||||||
target: target,
|
target: target,
|
||||||
|
runEnvironment: runEnvironment,
|
||||||
).run;
|
).run;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -768,11 +769,17 @@ Future<void> _resetManifest(String testDirectory) async {
|
|||||||
|
|
||||||
/// Measure application startup performance.
|
/// Measure application startup performance.
|
||||||
class StartupTest {
|
class StartupTest {
|
||||||
const StartupTest(this.testDirectory, { this.reportMetrics = true, this.target = 'lib/main.dart' });
|
const StartupTest(
|
||||||
|
this.testDirectory, {
|
||||||
|
this.reportMetrics = true,
|
||||||
|
this.target = 'lib/main.dart',
|
||||||
|
this.runEnvironment,
|
||||||
|
});
|
||||||
|
|
||||||
final String testDirectory;
|
final String testDirectory;
|
||||||
final bool reportMetrics;
|
final bool reportMetrics;
|
||||||
final String target;
|
final String target;
|
||||||
|
final Map<String, String>? runEnvironment;
|
||||||
|
|
||||||
Future<TaskResult> run() async {
|
Future<TaskResult> run() async {
|
||||||
return inDirectory<TaskResult>(testDirectory, () async {
|
return inDirectory<TaskResult>(testDirectory, () async {
|
||||||
@ -855,21 +862,26 @@ class StartupTest {
|
|||||||
'screenshot_startup_${DateTime.now().toLocal().toIso8601String()}.png',
|
'screenshot_startup_${DateTime.now().toLocal().toIso8601String()}.png',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
final int result = await flutter('run', options: <String>[
|
final int result = await flutter(
|
||||||
'--no-android-gradle-daemon',
|
'run',
|
||||||
'--no-publish-port',
|
options: <String>[
|
||||||
'--verbose',
|
'--no-android-gradle-daemon',
|
||||||
'--profile',
|
'--no-publish-port',
|
||||||
'--trace-startup',
|
'--verbose',
|
||||||
// TODO(vashworth): Remove once done debugging https://github.com/flutter/flutter/issues/129836
|
'--profile',
|
||||||
if (device is IosDevice)
|
'--trace-startup',
|
||||||
'--verbose-system-logs',
|
// TODO(vashworth): Remove once done debugging https://github.com/flutter/flutter/issues/129836
|
||||||
'--target=$target',
|
if (device is IosDevice)
|
||||||
'-d',
|
'--verbose-system-logs',
|
||||||
device.deviceId,
|
'--target=$target',
|
||||||
if (applicationBinaryPath != null)
|
'-d',
|
||||||
'--use-application-binary=$applicationBinaryPath',
|
device.deviceId,
|
||||||
], canFail: true);
|
if (applicationBinaryPath != null)
|
||||||
|
'--use-application-binary=$applicationBinaryPath',
|
||||||
|
],
|
||||||
|
environment: runEnvironment,
|
||||||
|
canFail: true,
|
||||||
|
);
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
if (result == 0) {
|
if (result == 0) {
|
||||||
final Map<String, dynamic> data = json.decode(
|
final Map<String, dynamic> data = json.decode(
|
||||||
|
530
packages/flutter_tools/bin/xcode_debug.js
Normal file
530
packages/flutter_tools/bin/xcode_debug.js
Normal file
@ -0,0 +1,530 @@
|
|||||||
|
// 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.
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @fileoverview OSA Script to interact with Xcode. Functionality includes
|
||||||
|
* checking if a given project is open in Xcode, starting a debug session for
|
||||||
|
* a given project, and stopping a debug session for a given project.
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OSA Script `run` handler that is called when the script is run. When ran
|
||||||
|
* with `osascript`, arguments are passed from the command line to the direct
|
||||||
|
* parameter of the `run` handler as a list of strings.
|
||||||
|
*
|
||||||
|
* @param {?Array<string>=} args_array
|
||||||
|
* @returns {!RunJsonResponse} The validated command.
|
||||||
|
*/
|
||||||
|
function run(args_array = []) {
|
||||||
|
let args;
|
||||||
|
try {
|
||||||
|
args = new CommandArguments(args_array);
|
||||||
|
} catch (e) {
|
||||||
|
return new RunJsonResponse(false, `Failed to parse arguments: ${e}`).stringify();
|
||||||
|
}
|
||||||
|
|
||||||
|
const xcodeResult = getXcode(args);
|
||||||
|
if (xcodeResult.error != null) {
|
||||||
|
return new RunJsonResponse(false, xcodeResult.error).stringify();
|
||||||
|
}
|
||||||
|
const xcode = xcodeResult.result;
|
||||||
|
|
||||||
|
if (args.command === 'check-workspace-opened') {
|
||||||
|
const result = getWorkspaceDocument(xcode, args);
|
||||||
|
return new RunJsonResponse(result.error == null, result.error).stringify();
|
||||||
|
} else if (args.command === 'debug') {
|
||||||
|
const result = debugApp(xcode, args);
|
||||||
|
return new RunJsonResponse(result.error == null, result.error, result.result).stringify();
|
||||||
|
} else if (args.command === 'stop') {
|
||||||
|
const result = stopApp(xcode, args);
|
||||||
|
return new RunJsonResponse(result.error == null, result.error).stringify();
|
||||||
|
} else {
|
||||||
|
return new RunJsonResponse(false, 'Unknown command').stringify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsed and validated arguments passed from the command line.
|
||||||
|
*/
|
||||||
|
class CommandArguments {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {!Array<string>} args List of arguments passed from the command line.
|
||||||
|
*/
|
||||||
|
constructor(args) {
|
||||||
|
this.command = this.validatedCommand(args[0]);
|
||||||
|
|
||||||
|
const parsedArguments = this.parseArguments(args);
|
||||||
|
|
||||||
|
this.xcodePath = this.validatedStringArgument('--xcode-path', parsedArguments['--xcode-path']);
|
||||||
|
this.projectPath = this.validatedStringArgument('--project-path', parsedArguments['--project-path']);
|
||||||
|
this.workspacePath = this.validatedStringArgument('--workspace-path', parsedArguments['--workspace-path']);
|
||||||
|
this.targetDestinationId = this.validatedStringArgument('--device-id', parsedArguments['--device-id']);
|
||||||
|
this.targetSchemeName = this.validatedStringArgument('--scheme', parsedArguments['--scheme']);
|
||||||
|
this.skipBuilding = this.validatedBoolArgument('--skip-building', parsedArguments['--skip-building']);
|
||||||
|
this.launchArguments = this.validatedJsonArgument('--launch-args', parsedArguments['--launch-args']);
|
||||||
|
this.closeWindowOnStop = this.validatedBoolArgument('--close-window', parsedArguments['--close-window']);
|
||||||
|
this.promptToSaveBeforeClose = this.validatedBoolArgument('--prompt-to-save', parsedArguments['--prompt-to-save']);
|
||||||
|
this.verbose = this.validatedBoolArgument('--verbose', parsedArguments['--verbose']);
|
||||||
|
|
||||||
|
if (this.verbose === true) {
|
||||||
|
console.log(JSON.stringify(this));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the command is available.
|
||||||
|
*
|
||||||
|
* @param {?string} command
|
||||||
|
* @returns {!string} The validated command.
|
||||||
|
* @throws Will throw an error if command is not recognized.
|
||||||
|
*/
|
||||||
|
validatedCommand(command) {
|
||||||
|
const allowedCommands = ['check-workspace-opened', 'debug', 'stop'];
|
||||||
|
if (allowedCommands.includes(command) === false) {
|
||||||
|
throw `Unrecognized Command: ${command}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the flag is allowed for the current command.
|
||||||
|
*
|
||||||
|
* @param {!string} flag
|
||||||
|
* @param {?string} value
|
||||||
|
* @returns {!bool}
|
||||||
|
* @throws Will throw an error if the flag is not allowed for the current
|
||||||
|
* command and the value is not null, undefined, or empty.
|
||||||
|
*/
|
||||||
|
isArgumentAllowed(flag, value) {
|
||||||
|
const allowedArguments = {
|
||||||
|
'common': {
|
||||||
|
'--xcode-path': true,
|
||||||
|
'--project-path': true,
|
||||||
|
'--workspace-path': true,
|
||||||
|
'--verbose': true,
|
||||||
|
},
|
||||||
|
'check-workspace-opened': {},
|
||||||
|
'debug': {
|
||||||
|
'--device-id': true,
|
||||||
|
'--scheme': true,
|
||||||
|
'--skip-building': true,
|
||||||
|
'--launch-args': true,
|
||||||
|
},
|
||||||
|
'stop': {
|
||||||
|
'--close-window': true,
|
||||||
|
'--prompt-to-save': true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAllowed = allowedArguments['common'][flag] === true || allowedArguments[this.command][flag] === true;
|
||||||
|
if (isAllowed === false && (value != null && value !== '')) {
|
||||||
|
throw `The flag ${flag} is not allowed for the command ${this.command}.`;
|
||||||
|
}
|
||||||
|
return isAllowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses the command line arguments into an object.
|
||||||
|
*
|
||||||
|
* @param {!Array<string>} args List of arguments passed from the command line.
|
||||||
|
* @returns {!Object.<string, string>} Object mapping flag to value.
|
||||||
|
* @throws Will throw an error if flag does not begin with '--'.
|
||||||
|
*/
|
||||||
|
parseArguments(args) {
|
||||||
|
const valuesPerFlag = {};
|
||||||
|
for (let index = 1; index < args.length; index++) {
|
||||||
|
const entry = args[index];
|
||||||
|
let flag;
|
||||||
|
let value;
|
||||||
|
const splitIndex = entry.indexOf('=');
|
||||||
|
if (splitIndex === -1) {
|
||||||
|
flag = entry;
|
||||||
|
value = args[index + 1];
|
||||||
|
|
||||||
|
// If the flag is allowed for the command, and the next value in the
|
||||||
|
// array is null/undefined or also a flag, treat the flag like a boolean
|
||||||
|
// flag and set the value to 'true'.
|
||||||
|
if (this.isArgumentAllowed(flag) && (value == null || value.startsWith('--'))) {
|
||||||
|
value = 'true';
|
||||||
|
} else {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
flag = entry.substring(0, splitIndex);
|
||||||
|
value = entry.substring(splitIndex + 1, entry.length + 1);
|
||||||
|
}
|
||||||
|
if (flag.startsWith('--') === false) {
|
||||||
|
throw `Unrecognized Flag: ${flag}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
valuesPerFlag[flag] = value;
|
||||||
|
}
|
||||||
|
return valuesPerFlag;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the flag is allowed and `value` is valid. If the flag is not
|
||||||
|
* allowed for the current command, return `null`.
|
||||||
|
*
|
||||||
|
* @param {!string} flag
|
||||||
|
* @param {?string} value
|
||||||
|
* @returns {!string}
|
||||||
|
* @throws Will throw an error if the flag is allowed and `value` is null,
|
||||||
|
* undefined, or empty.
|
||||||
|
*/
|
||||||
|
validatedStringArgument(flag, value) {
|
||||||
|
if (this.isArgumentAllowed(flag, value) === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value == null || value === '') {
|
||||||
|
throw `Missing value for ${flag}`;
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the flag is allowed, validates `value` is valid, and converts
|
||||||
|
* `value` to a boolean. A `value` of null, undefined, or empty, it will
|
||||||
|
* return true. If the flag is not allowed for the current command, will
|
||||||
|
* return `null`.
|
||||||
|
*
|
||||||
|
* @param {?string} value
|
||||||
|
* @returns {?boolean}
|
||||||
|
* @throws Will throw an error if the flag is allowed and `value` is not
|
||||||
|
* null, undefined, empty, 'true', or 'false'.
|
||||||
|
*/
|
||||||
|
validatedBoolArgument(flag, value) {
|
||||||
|
if (this.isArgumentAllowed(flag, value) === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value == null || value === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (value !== 'true' && value !== 'false') {
|
||||||
|
throw `Invalid value for ${flag}`;
|
||||||
|
}
|
||||||
|
return value === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the flag is allowed, `value` is valid, and parses `value` as JSON.
|
||||||
|
* If the flag is not allowed for the current command, will return `null`.
|
||||||
|
*
|
||||||
|
* @param {!string} flag
|
||||||
|
* @param {?string} value
|
||||||
|
* @returns {!Object}
|
||||||
|
* @throws Will throw an error if the flag is allowed and the value is
|
||||||
|
* null, undefined, or empty. Will also throw an error if parsing fails.
|
||||||
|
*/
|
||||||
|
validatedJsonArgument(flag, value) {
|
||||||
|
if (this.isArgumentAllowed(flag, value) === false) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value == null || value === '') {
|
||||||
|
throw `Missing value for ${flag}`;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return JSON.parse(value);
|
||||||
|
} catch (e) {
|
||||||
|
throw `Error parsing ${flag}: ${e}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response to return in `run` function.
|
||||||
|
*/
|
||||||
|
class RunJsonResponse {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {!bool} success Whether the command was successful.
|
||||||
|
* @param {?string=} errorMessage Defaults to null.
|
||||||
|
* @param {?DebugResult=} debugResult Curated results from Xcode's debug
|
||||||
|
* function. Defaults to null.
|
||||||
|
*/
|
||||||
|
constructor(success, errorMessage = null, debugResult = null) {
|
||||||
|
this.status = success;
|
||||||
|
this.errorMessage = errorMessage;
|
||||||
|
this.debugResult = debugResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts this object to a JSON string.
|
||||||
|
*
|
||||||
|
* @returns {!string}
|
||||||
|
* @throws Throws an error if conversion fails.
|
||||||
|
*/
|
||||||
|
stringify() {
|
||||||
|
return JSON.stringify(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class to return a result along with a potential error.
|
||||||
|
*/
|
||||||
|
class FunctionResult {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {?Object} result
|
||||||
|
* @param {?string=} error Defaults to null.
|
||||||
|
*/
|
||||||
|
constructor(result, error = null) {
|
||||||
|
this.result = result;
|
||||||
|
this.error = error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Curated results from Xcode's debug function. Mirrors parts of
|
||||||
|
* `scheme action result` from Xcode's Script Editor dictionary.
|
||||||
|
*/
|
||||||
|
class DebugResult {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {!Object} result
|
||||||
|
*/
|
||||||
|
constructor(result) {
|
||||||
|
this.completed = result.completed();
|
||||||
|
this.status = result.status();
|
||||||
|
this.errorMessage = result.errorMessage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Xcode application from the given path. Since macs can have multiple
|
||||||
|
* Xcode version, we use the path to target the specific Xcode application.
|
||||||
|
* If the Xcode app is not running, return null with an error.
|
||||||
|
*
|
||||||
|
* @param {!CommandArguments} args
|
||||||
|
* @returns {!FunctionResult} Return either an `Application` (Mac Scripting class)
|
||||||
|
* or null as the `result`.
|
||||||
|
*/
|
||||||
|
function getXcode(args) {
|
||||||
|
try {
|
||||||
|
const xcode = Application(args.xcodePath);
|
||||||
|
const isXcodeRunning = xcode.running();
|
||||||
|
|
||||||
|
if (isXcodeRunning === false) {
|
||||||
|
return new FunctionResult(null, 'Xcode is not running');
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FunctionResult(xcode);
|
||||||
|
} catch (e) {
|
||||||
|
return new FunctionResult(null, `Failed to get Xcode application: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After setting the active run destination to the targeted device, uses Xcode
|
||||||
|
* debug function from Mac Scripting for Xcode to install the app on the device
|
||||||
|
* and start a debugging session using the 'run' or 'run without building' scheme
|
||||||
|
* action (depending on `args.skipBuilding`). Waits for the debugging session
|
||||||
|
* to start running.
|
||||||
|
*
|
||||||
|
* @param {!Application} xcode An `Application` (Mac Scripting class) for Xcode.
|
||||||
|
* @param {!CommandArguments} args
|
||||||
|
* @returns {!FunctionResult} Return either a `DebugResult` or null as the `result`.
|
||||||
|
*/
|
||||||
|
function debugApp(xcode, args) {
|
||||||
|
const workspaceResult = waitForWorkspaceToLoad(xcode, args);
|
||||||
|
if (workspaceResult.error != null) {
|
||||||
|
return new FunctionResult(null, workspaceResult.error);
|
||||||
|
}
|
||||||
|
const targetWorkspace = workspaceResult.result;
|
||||||
|
|
||||||
|
const destinationResult = getTargetDestination(
|
||||||
|
targetWorkspace,
|
||||||
|
args.targetDestinationId,
|
||||||
|
args.verbose,
|
||||||
|
);
|
||||||
|
if (destinationResult.error != null) {
|
||||||
|
return new FunctionResult(null, destinationResult.error)
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Documentation from the Xcode Script Editor dictionary indicates that the
|
||||||
|
// `debug` function has a parameter called `runDestinationSpecifier` which
|
||||||
|
// is used to specify which device to debug the app on. It also states that
|
||||||
|
// it should be the same as the xcodebuild -destination specifier. It also
|
||||||
|
// states that if not specified, the `activeRunDestination` is used instead.
|
||||||
|
//
|
||||||
|
// Experimentation has shown that the `runDestinationSpecifier` does not work.
|
||||||
|
// It will always use the `activeRunDestination`. To mitigate this, we set
|
||||||
|
// the `activeRunDestination` to the targeted device prior to starting the debug.
|
||||||
|
targetWorkspace.activeRunDestination = destinationResult.result;
|
||||||
|
|
||||||
|
const actionResult = targetWorkspace.debug({
|
||||||
|
scheme: args.targetSchemeName,
|
||||||
|
skipBuilding: args.skipBuilding,
|
||||||
|
commandLineArguments: args.launchArguments,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait until scheme action has started up to a max of 10 minutes.
|
||||||
|
// This does not wait for app to install, launch, or start debug session.
|
||||||
|
// Potential statuses include: not yet started/running/cancelled/failed/error occurred/succeeded.
|
||||||
|
const checkFrequencyInSeconds = 0.5;
|
||||||
|
const maxWaitInSeconds = 10 * 60; // 10 minutes
|
||||||
|
const iterations = maxWaitInSeconds * (1 / checkFrequencyInSeconds);
|
||||||
|
const verboseLogInterval = 10 * (1 / checkFrequencyInSeconds);
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
if (actionResult.status() !== 'not yet started') {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (args.verbose === true && i % verboseLogInterval === 0) {
|
||||||
|
console.log(`Action result status: ${actionResult.status()}`);
|
||||||
|
}
|
||||||
|
delay(checkFrequencyInSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new FunctionResult(new DebugResult(actionResult));
|
||||||
|
} catch (e) {
|
||||||
|
return new FunctionResult(null, `Failed to start debugging session: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Iterates through available run destinations looking for one with a matching
|
||||||
|
* `deviceId`. If device is not found, return null with an error.
|
||||||
|
*
|
||||||
|
* @param {!WorkspaceDocument} targetWorkspace A `WorkspaceDocument` (Xcode Mac
|
||||||
|
* Scripting class).
|
||||||
|
* @param {!string} deviceId
|
||||||
|
* @param {?bool=} verbose Defaults to false.
|
||||||
|
* @returns {!FunctionResult} Return either a `RunDestination` (Xcode Mac
|
||||||
|
* Scripting class) or null as the `result`.
|
||||||
|
*/
|
||||||
|
function getTargetDestination(targetWorkspace, deviceId, verbose = false) {
|
||||||
|
try {
|
||||||
|
for (let destination of targetWorkspace.runDestinations()) {
|
||||||
|
const device = destination.device();
|
||||||
|
if (verbose === true && device != null) {
|
||||||
|
console.log(`Device: ${device.name()} (${device.deviceIdentifier()})`);
|
||||||
|
}
|
||||||
|
if (device != null && device.deviceIdentifier() === deviceId) {
|
||||||
|
return new FunctionResult(destination);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new FunctionResult(
|
||||||
|
null,
|
||||||
|
'Unable to find target device. Ensure that the device is paired, ' +
|
||||||
|
'unlocked, connected, and has an iOS version at least as high as the ' +
|
||||||
|
'Minimum Deployment.',
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return new FunctionResult(null, `Failed to get target destination: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for the workspace to load. If the workspace is not loaded or in the
|
||||||
|
* process of opening, it will wait up to 10 minutes.
|
||||||
|
*
|
||||||
|
* @param {!Application} xcode An `Application` (Mac Scripting class) for Xcode.
|
||||||
|
* @param {!CommandArguments} args
|
||||||
|
* @returns {!FunctionResult} Return either a `WorkspaceDocument` (Xcode Mac
|
||||||
|
* Scripting class) or null as the `result`.
|
||||||
|
*/
|
||||||
|
function waitForWorkspaceToLoad(xcode, args) {
|
||||||
|
try {
|
||||||
|
const checkFrequencyInSeconds = 0.5;
|
||||||
|
const maxWaitInSeconds = 10 * 60; // 10 minutes
|
||||||
|
const verboseLogInterval = 10 * (1 / checkFrequencyInSeconds);
|
||||||
|
const iterations = maxWaitInSeconds * (1 / checkFrequencyInSeconds);
|
||||||
|
for (let i = 0; i < iterations; i++) {
|
||||||
|
// Every 10 seconds, print the list of workspaces if verbose
|
||||||
|
const verbose = args.verbose && i % verboseLogInterval === 0;
|
||||||
|
|
||||||
|
const workspaceResult = getWorkspaceDocument(xcode, args, verbose);
|
||||||
|
if (workspaceResult.error == null) {
|
||||||
|
const document = workspaceResult.result;
|
||||||
|
if (document.loaded() === true) {
|
||||||
|
return new FunctionResult(document, null);
|
||||||
|
}
|
||||||
|
} else if (verbose === true) {
|
||||||
|
console.log(workspaceResult.error);
|
||||||
|
}
|
||||||
|
delay(checkFrequencyInSeconds);
|
||||||
|
}
|
||||||
|
return new FunctionResult(null, 'Timed out waiting for workspace to load');
|
||||||
|
} catch (e) {
|
||||||
|
return new FunctionResult(null, `Failed to wait for workspace to load: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets workspace opened in Xcode matching the projectPath or workspacePath
|
||||||
|
* from the command line arguments. If workspace is not found, return null with
|
||||||
|
* an error.
|
||||||
|
*
|
||||||
|
* @param {!Application} xcode An `Application` (Mac Scripting class) for Xcode.
|
||||||
|
* @param {!CommandArguments} args
|
||||||
|
* @param {?bool=} verbose Defaults to false.
|
||||||
|
* @returns {!FunctionResult} Return either a `WorkspaceDocument` (Xcode Mac
|
||||||
|
* Scripting class) or null as the `result`.
|
||||||
|
*/
|
||||||
|
function getWorkspaceDocument(xcode, args, verbose = false) {
|
||||||
|
const privatePrefix = '/private';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const documents = xcode.workspaceDocuments();
|
||||||
|
for (let document of documents) {
|
||||||
|
const filePath = document.file().toString();
|
||||||
|
if (verbose === true) {
|
||||||
|
console.log(`Workspace: ${filePath}`);
|
||||||
|
}
|
||||||
|
if (filePath === args.projectPath || filePath === args.workspacePath) {
|
||||||
|
return new FunctionResult(document);
|
||||||
|
}
|
||||||
|
// Sometimes when the project is in a temporary directory, it'll be
|
||||||
|
// prefixed with `/private` but the args will not. Remove the
|
||||||
|
// prefix before matching.
|
||||||
|
if (filePath.startsWith(privatePrefix) === true) {
|
||||||
|
const filePathWithoutPrefix = filePath.slice(privatePrefix.length);
|
||||||
|
if (filePathWithoutPrefix === args.projectPath || filePathWithoutPrefix === args.workspacePath) {
|
||||||
|
return new FunctionResult(document);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return new FunctionResult(null, `Failed to get workspace: ${e}`);
|
||||||
|
}
|
||||||
|
return new FunctionResult(null, `Failed to get workspace.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops all debug sessions in the target workspace.
|
||||||
|
*
|
||||||
|
* @param {!Application} xcode An `Application` (Mac Scripting class) for Xcode.
|
||||||
|
* @param {!CommandArguments} args
|
||||||
|
* @returns {!FunctionResult} Always returns null as the `result`.
|
||||||
|
*/
|
||||||
|
function stopApp(xcode, args) {
|
||||||
|
const workspaceResult = getWorkspaceDocument(xcode, args);
|
||||||
|
if (workspaceResult.error != null) {
|
||||||
|
return new FunctionResult(null, workspaceResult.error);
|
||||||
|
}
|
||||||
|
const targetDocument = workspaceResult.result;
|
||||||
|
|
||||||
|
try {
|
||||||
|
targetDocument.stop();
|
||||||
|
|
||||||
|
if (args.closeWindowOnStop === true) {
|
||||||
|
// Wait a couple seconds before closing Xcode, otherwise it'll prompt the
|
||||||
|
// user to stop the app.
|
||||||
|
delay(2);
|
||||||
|
|
||||||
|
targetDocument.close({
|
||||||
|
saving: args.promptToSaveBeforeClose === true ? 'ask' : 'no',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
return new FunctionResult(null, `Failed to stop app: ${e}`);
|
||||||
|
}
|
||||||
|
return new FunctionResult(null, null);
|
||||||
|
}
|
@ -359,6 +359,7 @@ Future<T> runInContext<T>(
|
|||||||
platform: globals.platform,
|
platform: globals.platform,
|
||||||
fileSystem: globals.fs,
|
fileSystem: globals.fs,
|
||||||
xcodeProjectInterpreter: globals.xcodeProjectInterpreter!,
|
xcodeProjectInterpreter: globals.xcodeProjectInterpreter!,
|
||||||
|
userMessages: globals.userMessages,
|
||||||
),
|
),
|
||||||
XCDevice: () => XCDevice(
|
XCDevice: () => XCDevice(
|
||||||
processManager: globals.processManager,
|
processManager: globals.processManager,
|
||||||
@ -375,6 +376,7 @@ Future<T> runInContext<T>(
|
|||||||
processManager: globals.processManager,
|
processManager: globals.processManager,
|
||||||
dyLdLibEntry: globals.cache.dyLdLibEntry,
|
dyLdLibEntry: globals.cache.dyLdLibEntry,
|
||||||
),
|
),
|
||||||
|
fileSystem: globals.fs,
|
||||||
),
|
),
|
||||||
XcodeProjectInterpreter: () => XcodeProjectInterpreter(
|
XcodeProjectInterpreter: () => XcodeProjectInterpreter(
|
||||||
logger: globals.logger,
|
logger: globals.logger,
|
||||||
|
@ -1159,6 +1159,7 @@ class DebuggingOptions {
|
|||||||
Map<String, Object?> platformArgs, {
|
Map<String, Object?> platformArgs, {
|
||||||
bool ipv6 = false,
|
bool ipv6 = false,
|
||||||
DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached,
|
DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached,
|
||||||
|
bool isCoreDevice = false,
|
||||||
}) {
|
}) {
|
||||||
final String dartVmFlags = computeDartVmFlags(this);
|
final String dartVmFlags = computeDartVmFlags(this);
|
||||||
return <String>[
|
return <String>[
|
||||||
@ -1172,7 +1173,10 @@ class DebuggingOptions {
|
|||||||
if (environmentType == EnvironmentType.simulator && dartVmFlags.isNotEmpty)
|
if (environmentType == EnvironmentType.simulator && dartVmFlags.isNotEmpty)
|
||||||
'--dart-flags=$dartVmFlags',
|
'--dart-flags=$dartVmFlags',
|
||||||
if (useTestFonts) '--use-test-fonts',
|
if (useTestFonts) '--use-test-fonts',
|
||||||
if (debuggingEnabled) ...<String>[
|
// Core Devices (iOS 17 devices) are debugged through Xcode so don't
|
||||||
|
// include these flags, which are used to check if the app was launched
|
||||||
|
// via Flutter CLI and `ios-deploy`.
|
||||||
|
if (debuggingEnabled && !isCoreDevice) ...<String>[
|
||||||
'--enable-checked-mode',
|
'--enable-checked-mode',
|
||||||
'--verify-entry-points',
|
'--verify-entry-points',
|
||||||
],
|
],
|
||||||
|
854
packages/flutter_tools/lib/src/ios/core_devices.dart
Normal file
854
packages/flutter_tools/lib/src/ios/core_devices.dart
Normal file
@ -0,0 +1,854 @@
|
|||||||
|
// 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 'package:meta/meta.dart';
|
||||||
|
import 'package:process/process.dart';
|
||||||
|
|
||||||
|
import '../base/file_system.dart';
|
||||||
|
import '../base/io.dart';
|
||||||
|
import '../base/logger.dart';
|
||||||
|
import '../base/process.dart';
|
||||||
|
import '../convert.dart';
|
||||||
|
import '../device.dart';
|
||||||
|
import '../macos/xcode.dart';
|
||||||
|
|
||||||
|
/// A wrapper around the `devicectl` command line tool.
|
||||||
|
///
|
||||||
|
/// CoreDevice is a device connectivity stack introduced in Xcode 15. Devices
|
||||||
|
/// with iOS 17 or greater are CoreDevices.
|
||||||
|
///
|
||||||
|
/// `devicectl` (CoreDevice Device Control) is an Xcode CLI tool used for
|
||||||
|
/// interacting with CoreDevices.
|
||||||
|
class IOSCoreDeviceControl {
|
||||||
|
IOSCoreDeviceControl({
|
||||||
|
required Logger logger,
|
||||||
|
required ProcessManager processManager,
|
||||||
|
required Xcode xcode,
|
||||||
|
required FileSystem fileSystem,
|
||||||
|
}) : _logger = logger,
|
||||||
|
_processUtils = ProcessUtils(logger: logger, processManager: processManager),
|
||||||
|
_xcode = xcode,
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
|
||||||
|
final Logger _logger;
|
||||||
|
final ProcessUtils _processUtils;
|
||||||
|
final Xcode _xcode;
|
||||||
|
final FileSystem _fileSystem;
|
||||||
|
|
||||||
|
/// When the `--timeout` flag is used with `devicectl`, it must be at
|
||||||
|
/// least 5 seconds. If lower than 5 seconds, `devicectl` will error and not
|
||||||
|
/// run the command.
|
||||||
|
static const int _minimumTimeoutInSeconds = 5;
|
||||||
|
|
||||||
|
/// Executes `devicectl` command to get list of devices. The command will
|
||||||
|
/// likely complete before [timeout] is reached. If [timeout] is reached,
|
||||||
|
/// the command will be stopped as a failure.
|
||||||
|
Future<List<Object?>> _listCoreDevices({
|
||||||
|
Duration timeout = const Duration(seconds: _minimumTimeoutInSeconds),
|
||||||
|
}) async {
|
||||||
|
if (!_xcode.isDevicectlInstalled) {
|
||||||
|
_logger.printError('devicectl is not installed.');
|
||||||
|
return <Object?>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to minimum timeout if needed to prevent error.
|
||||||
|
Duration validTimeout = timeout;
|
||||||
|
if (timeout.inSeconds < _minimumTimeoutInSeconds) {
|
||||||
|
_logger.printError(
|
||||||
|
'Timeout of ${timeout.inSeconds} seconds is below the minimum timeout value '
|
||||||
|
'for devicectl. Changing the timeout to the minimum value of $_minimumTimeoutInSeconds.');
|
||||||
|
validTimeout = const Duration(seconds: _minimumTimeoutInSeconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
|
||||||
|
final File output = tempDirectory.childFile('core_device_list.json');
|
||||||
|
output.createSync();
|
||||||
|
|
||||||
|
final List<String> command = <String>[
|
||||||
|
..._xcode.xcrunCommand(),
|
||||||
|
'devicectl',
|
||||||
|
'list',
|
||||||
|
'devices',
|
||||||
|
'--timeout',
|
||||||
|
validTimeout.inSeconds.toString(),
|
||||||
|
'--json-output',
|
||||||
|
output.path,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _processUtils.run(command, throwOnError: true);
|
||||||
|
|
||||||
|
final String stringOutput = output.readAsStringSync();
|
||||||
|
_logger.printTrace(stringOutput);
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['result'];
|
||||||
|
if (decodeResult is Map<String, Object?>) {
|
||||||
|
final Object? decodeDevices = decodeResult['devices'];
|
||||||
|
if (decodeDevices is List<Object?>) {
|
||||||
|
return decodeDevices;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_logger.printError('devicectl returned unexpected JSON response: $stringOutput');
|
||||||
|
return <Object?>[];
|
||||||
|
} on FormatException {
|
||||||
|
// We failed to parse the devicectl output, or it returned junk.
|
||||||
|
_logger.printError('devicectl returned non-JSON response: $stringOutput');
|
||||||
|
return <Object?>[];
|
||||||
|
}
|
||||||
|
} on ProcessException catch (err) {
|
||||||
|
_logger.printError('Error executing devicectl: $err');
|
||||||
|
return <Object?>[];
|
||||||
|
} finally {
|
||||||
|
tempDirectory.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<IOSCoreDevice>> getCoreDevices({
|
||||||
|
Duration timeout = const Duration(seconds: _minimumTimeoutInSeconds),
|
||||||
|
}) async {
|
||||||
|
final List<IOSCoreDevice> devices = <IOSCoreDevice>[];
|
||||||
|
|
||||||
|
final List<Object?> devicesSection = await _listCoreDevices(timeout: timeout);
|
||||||
|
for (final Object? deviceObject in devicesSection) {
|
||||||
|
if (deviceObject is Map<String, Object?>) {
|
||||||
|
devices.add(IOSCoreDevice.fromBetaJson(deviceObject, logger: _logger));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Executes `devicectl` command to get list of apps installed on the device.
|
||||||
|
/// If [bundleId] is provided, it will only return apps matching the bundle
|
||||||
|
/// identifier exactly.
|
||||||
|
Future<List<Object?>> _listInstalledApps({
|
||||||
|
required String deviceId,
|
||||||
|
String? bundleId,
|
||||||
|
}) async {
|
||||||
|
if (!_xcode.isDevicectlInstalled) {
|
||||||
|
_logger.printError('devicectl is not installed.');
|
||||||
|
return <Object?>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
|
||||||
|
final File output = tempDirectory.childFile('core_device_app_list.json');
|
||||||
|
output.createSync();
|
||||||
|
|
||||||
|
final List<String> command = <String>[
|
||||||
|
..._xcode.xcrunCommand(),
|
||||||
|
'devicectl',
|
||||||
|
'device',
|
||||||
|
'info',
|
||||||
|
'apps',
|
||||||
|
'--device',
|
||||||
|
deviceId,
|
||||||
|
if (bundleId != null)
|
||||||
|
'--bundle-id',
|
||||||
|
bundleId!,
|
||||||
|
'--json-output',
|
||||||
|
output.path,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _processUtils.run(command, throwOnError: true);
|
||||||
|
|
||||||
|
final String stringOutput = output.readAsStringSync();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['result'];
|
||||||
|
if (decodeResult is Map<String, Object?>) {
|
||||||
|
final Object? decodeApps = decodeResult['apps'];
|
||||||
|
if (decodeApps is List<Object?>) {
|
||||||
|
return decodeApps;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_logger.printError('devicectl returned unexpected JSON response: $stringOutput');
|
||||||
|
return <Object?>[];
|
||||||
|
} on FormatException {
|
||||||
|
// We failed to parse the devicectl output, or it returned junk.
|
||||||
|
_logger.printError('devicectl returned non-JSON response: $stringOutput');
|
||||||
|
return <Object?>[];
|
||||||
|
}
|
||||||
|
} on ProcessException catch (err) {
|
||||||
|
_logger.printError('Error executing devicectl: $err');
|
||||||
|
return <Object?>[];
|
||||||
|
} finally {
|
||||||
|
tempDirectory.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
Future<List<IOSCoreDeviceInstalledApp>> getInstalledApps({
|
||||||
|
required String deviceId,
|
||||||
|
String? bundleId,
|
||||||
|
}) async {
|
||||||
|
final List<IOSCoreDeviceInstalledApp> apps = <IOSCoreDeviceInstalledApp>[];
|
||||||
|
|
||||||
|
final List<Object?> appsData = await _listInstalledApps(deviceId: deviceId, bundleId: bundleId);
|
||||||
|
for (final Object? appObject in appsData) {
|
||||||
|
if (appObject is Map<String, Object?>) {
|
||||||
|
apps.add(IOSCoreDeviceInstalledApp.fromBetaJson(appObject));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> isAppInstalled({
|
||||||
|
required String deviceId,
|
||||||
|
required String bundleId,
|
||||||
|
}) async {
|
||||||
|
final List<IOSCoreDeviceInstalledApp> apps = await getInstalledApps(
|
||||||
|
deviceId: deviceId,
|
||||||
|
bundleId: bundleId,
|
||||||
|
);
|
||||||
|
if (apps.isNotEmpty) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> installApp({
|
||||||
|
required String deviceId,
|
||||||
|
required String bundlePath,
|
||||||
|
}) async {
|
||||||
|
if (!_xcode.isDevicectlInstalled) {
|
||||||
|
_logger.printError('devicectl is not installed.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
|
||||||
|
final File output = tempDirectory.childFile('install_results.json');
|
||||||
|
output.createSync();
|
||||||
|
|
||||||
|
final List<String> command = <String>[
|
||||||
|
..._xcode.xcrunCommand(),
|
||||||
|
'devicectl',
|
||||||
|
'device',
|
||||||
|
'install',
|
||||||
|
'app',
|
||||||
|
'--device',
|
||||||
|
deviceId,
|
||||||
|
bundlePath,
|
||||||
|
'--json-output',
|
||||||
|
output.path,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _processUtils.run(command, throwOnError: true);
|
||||||
|
final String stringOutput = output.readAsStringSync();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['info'];
|
||||||
|
if (decodeResult is Map<String, Object?> && decodeResult['outcome'] == 'success') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_logger.printError('devicectl returned unexpected JSON response: $stringOutput');
|
||||||
|
return false;
|
||||||
|
} on FormatException {
|
||||||
|
// We failed to parse the devicectl output, or it returned junk.
|
||||||
|
_logger.printError('devicectl returned non-JSON response: $stringOutput');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} on ProcessException catch (err) {
|
||||||
|
_logger.printError('Error executing devicectl: $err');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
tempDirectory.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Uninstalls the app from the device. Will succeed even if the app is not
|
||||||
|
/// currently installed on the device.
|
||||||
|
Future<bool> uninstallApp({
|
||||||
|
required String deviceId,
|
||||||
|
required String bundleId,
|
||||||
|
}) async {
|
||||||
|
if (!_xcode.isDevicectlInstalled) {
|
||||||
|
_logger.printError('devicectl is not installed.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
|
||||||
|
final File output = tempDirectory.childFile('uninstall_results.json');
|
||||||
|
output.createSync();
|
||||||
|
|
||||||
|
final List<String> command = <String>[
|
||||||
|
..._xcode.xcrunCommand(),
|
||||||
|
'devicectl',
|
||||||
|
'device',
|
||||||
|
'uninstall',
|
||||||
|
'app',
|
||||||
|
'--device',
|
||||||
|
deviceId,
|
||||||
|
bundleId,
|
||||||
|
'--json-output',
|
||||||
|
output.path,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _processUtils.run(command, throwOnError: true);
|
||||||
|
final String stringOutput = output.readAsStringSync();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['info'];
|
||||||
|
if (decodeResult is Map<String, Object?> && decodeResult['outcome'] == 'success') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_logger.printError('devicectl returned unexpected JSON response: $stringOutput');
|
||||||
|
return false;
|
||||||
|
} on FormatException {
|
||||||
|
// We failed to parse the devicectl output, or it returned junk.
|
||||||
|
_logger.printError('devicectl returned non-JSON response: $stringOutput');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} on ProcessException catch (err) {
|
||||||
|
_logger.printError('Error executing devicectl: $err');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
tempDirectory.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> launchApp({
|
||||||
|
required String deviceId,
|
||||||
|
required String bundleId,
|
||||||
|
List<String> launchArguments = const <String>[],
|
||||||
|
}) async {
|
||||||
|
if (!_xcode.isDevicectlInstalled) {
|
||||||
|
_logger.printError('devicectl is not installed.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Directory tempDirectory = _fileSystem.systemTempDirectory.createTempSync('core_devices.');
|
||||||
|
final File output = tempDirectory.childFile('launch_results.json');
|
||||||
|
output.createSync();
|
||||||
|
|
||||||
|
final List<String> command = <String>[
|
||||||
|
..._xcode.xcrunCommand(),
|
||||||
|
'devicectl',
|
||||||
|
'device',
|
||||||
|
'process',
|
||||||
|
'launch',
|
||||||
|
'--device',
|
||||||
|
deviceId,
|
||||||
|
bundleId,
|
||||||
|
if (launchArguments.isNotEmpty) ...launchArguments,
|
||||||
|
'--json-output',
|
||||||
|
output.path,
|
||||||
|
];
|
||||||
|
|
||||||
|
try {
|
||||||
|
await _processUtils.run(command, throwOnError: true);
|
||||||
|
final String stringOutput = output.readAsStringSync();
|
||||||
|
|
||||||
|
try {
|
||||||
|
final Object? decodeResult = (json.decode(stringOutput) as Map<String, Object?>)['info'];
|
||||||
|
if (decodeResult is Map<String, Object?> && decodeResult['outcome'] == 'success') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
_logger.printError('devicectl returned unexpected JSON response: $stringOutput');
|
||||||
|
return false;
|
||||||
|
} on FormatException {
|
||||||
|
// We failed to parse the devicectl output, or it returned junk.
|
||||||
|
_logger.printError('devicectl returned non-JSON response: $stringOutput');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} on ProcessException catch (err) {
|
||||||
|
_logger.printError('Error executing devicectl: $err');
|
||||||
|
return false;
|
||||||
|
} finally {
|
||||||
|
tempDirectory.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class IOSCoreDevice {
|
||||||
|
IOSCoreDevice._({
|
||||||
|
required this.capabilities,
|
||||||
|
required this.connectionProperties,
|
||||||
|
required this.deviceProperties,
|
||||||
|
required this.hardwareProperties,
|
||||||
|
required this.coreDeviceIdentifer,
|
||||||
|
required this.visibilityClass,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Parse JSON from `devicectl list devices --json-output` while it's in beta preview mode.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// {
|
||||||
|
/// "capabilities" : [
|
||||||
|
/// ],
|
||||||
|
/// "connectionProperties" : {
|
||||||
|
/// },
|
||||||
|
/// "deviceProperties" : {
|
||||||
|
/// },
|
||||||
|
/// "hardwareProperties" : {
|
||||||
|
/// },
|
||||||
|
/// "identifier" : "123456BB5-AEDE-7A22-B890-1234567890DD",
|
||||||
|
/// "visibilityClass" : "default"
|
||||||
|
/// }
|
||||||
|
factory IOSCoreDevice.fromBetaJson(
|
||||||
|
Map<String, Object?> data, {
|
||||||
|
required Logger logger,
|
||||||
|
}) {
|
||||||
|
final List<_IOSCoreDeviceCapability> capabilitiesList = <_IOSCoreDeviceCapability>[];
|
||||||
|
if (data['capabilities'] is List<Object?>) {
|
||||||
|
final List<Object?> capabilitiesData = data['capabilities']! as List<Object?>;
|
||||||
|
for (final Object? capabilityData in capabilitiesData) {
|
||||||
|
if (capabilityData != null && capabilityData is Map<String, Object?>) {
|
||||||
|
capabilitiesList.add(_IOSCoreDeviceCapability.fromBetaJson(capabilityData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_IOSCoreDeviceConnectionProperties? connectionProperties;
|
||||||
|
if (data['connectionProperties'] is Map<String, Object?>) {
|
||||||
|
final Map<String, Object?> connectionPropertiesData = data['connectionProperties']! as Map<String, Object?>;
|
||||||
|
connectionProperties = _IOSCoreDeviceConnectionProperties.fromBetaJson(
|
||||||
|
connectionPropertiesData,
|
||||||
|
logger: logger,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IOSCoreDeviceProperties? deviceProperties;
|
||||||
|
if (data['deviceProperties'] is Map<String, Object?>) {
|
||||||
|
final Map<String, Object?> devicePropertiesData = data['deviceProperties']! as Map<String, Object?>;
|
||||||
|
deviceProperties = IOSCoreDeviceProperties.fromBetaJson(devicePropertiesData);
|
||||||
|
}
|
||||||
|
|
||||||
|
_IOSCoreDeviceHardwareProperties? hardwareProperties;
|
||||||
|
if (data['hardwareProperties'] is Map<String, Object?>) {
|
||||||
|
final Map<String, Object?> hardwarePropertiesData = data['hardwareProperties']! as Map<String, Object?>;
|
||||||
|
hardwareProperties = _IOSCoreDeviceHardwareProperties.fromBetaJson(
|
||||||
|
hardwarePropertiesData,
|
||||||
|
logger: logger,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return IOSCoreDevice._(
|
||||||
|
capabilities: capabilitiesList,
|
||||||
|
connectionProperties: connectionProperties,
|
||||||
|
deviceProperties: deviceProperties,
|
||||||
|
hardwareProperties: hardwareProperties,
|
||||||
|
coreDeviceIdentifer: data['identifier']?.toString(),
|
||||||
|
visibilityClass: data['visibilityClass']?.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get udid => hardwareProperties?.udid;
|
||||||
|
|
||||||
|
DeviceConnectionInterface? get connectionInterface {
|
||||||
|
final String? transportType = connectionProperties?.transportType;
|
||||||
|
if (transportType != null) {
|
||||||
|
if (transportType.toLowerCase() == 'localnetwork') {
|
||||||
|
return DeviceConnectionInterface.wireless;
|
||||||
|
} else if (transportType.toLowerCase() == 'wired') {
|
||||||
|
return DeviceConnectionInterface.attached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
final List<_IOSCoreDeviceCapability> capabilities;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
final _IOSCoreDeviceConnectionProperties? connectionProperties;
|
||||||
|
|
||||||
|
final IOSCoreDeviceProperties? deviceProperties;
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
final _IOSCoreDeviceHardwareProperties? hardwareProperties;
|
||||||
|
|
||||||
|
final String? coreDeviceIdentifer;
|
||||||
|
final String? visibilityClass;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class _IOSCoreDeviceCapability {
|
||||||
|
_IOSCoreDeviceCapability._({
|
||||||
|
required this.featureIdentifier,
|
||||||
|
required this.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Parse `capabilities` section of JSON from `devicectl list devices --json-output`
|
||||||
|
/// while it's in beta preview mode.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// "capabilities" : [
|
||||||
|
/// {
|
||||||
|
/// "featureIdentifier" : "com.apple.coredevice.feature.spawnexecutable",
|
||||||
|
/// "name" : "Spawn Executable"
|
||||||
|
/// },
|
||||||
|
/// {
|
||||||
|
/// "featureIdentifier" : "com.apple.coredevice.feature.launchapplication",
|
||||||
|
/// "name" : "Launch Application"
|
||||||
|
/// }
|
||||||
|
/// ]
|
||||||
|
factory _IOSCoreDeviceCapability.fromBetaJson(Map<String, Object?> data) {
|
||||||
|
return _IOSCoreDeviceCapability._(
|
||||||
|
featureIdentifier: data['featureIdentifier']?.toString(),
|
||||||
|
name: data['name']?.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? featureIdentifier;
|
||||||
|
final String? name;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IOSCoreDeviceConnectionProperties {
|
||||||
|
_IOSCoreDeviceConnectionProperties._({
|
||||||
|
required this.authenticationType,
|
||||||
|
required this.isMobileDeviceOnly,
|
||||||
|
required this.lastConnectionDate,
|
||||||
|
required this.localHostnames,
|
||||||
|
required this.pairingState,
|
||||||
|
required this.potentialHostnames,
|
||||||
|
required this.transportType,
|
||||||
|
required this.tunnelIPAddress,
|
||||||
|
required this.tunnelState,
|
||||||
|
required this.tunnelTransportProtocol,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Parse `connectionProperties` section of JSON from `devicectl list devices --json-output`
|
||||||
|
/// while it's in beta preview mode.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// "connectionProperties" : {
|
||||||
|
/// "authenticationType" : "manualPairing",
|
||||||
|
/// "isMobileDeviceOnly" : false,
|
||||||
|
/// "lastConnectionDate" : "2023-06-15T15:29:00.082Z",
|
||||||
|
/// "localHostnames" : [
|
||||||
|
/// "iPadName.coredevice.local",
|
||||||
|
/// "00001234-0001234A3C03401E.coredevice.local",
|
||||||
|
/// "12345BB5-AEDE-4A22-B653-6037262550DD.coredevice.local"
|
||||||
|
/// ],
|
||||||
|
/// "pairingState" : "paired",
|
||||||
|
/// "potentialHostnames" : [
|
||||||
|
/// "00001234-0001234A3C03401E.coredevice.local",
|
||||||
|
/// "12345BB5-AEDE-4A22-B653-6037262550DD.coredevice.local"
|
||||||
|
/// ],
|
||||||
|
/// "transportType" : "wired",
|
||||||
|
/// "tunnelIPAddress" : "fdf1:23c4:cd56::1",
|
||||||
|
/// "tunnelState" : "connected",
|
||||||
|
/// "tunnelTransportProtocol" : "tcp"
|
||||||
|
/// }
|
||||||
|
factory _IOSCoreDeviceConnectionProperties.fromBetaJson(
|
||||||
|
Map<String, Object?> data, {
|
||||||
|
required Logger logger,
|
||||||
|
}) {
|
||||||
|
List<String>? localHostnames;
|
||||||
|
if (data['localHostnames'] is List<Object?>) {
|
||||||
|
final List<Object?> values = data['localHostnames']! as List<Object?>;
|
||||||
|
try {
|
||||||
|
localHostnames = List<String>.from(values);
|
||||||
|
} on TypeError {
|
||||||
|
logger.printTrace('Error parsing localHostnames value: $values');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String>? potentialHostnames;
|
||||||
|
if (data['potentialHostnames'] is List<Object?>) {
|
||||||
|
final List<Object?> values = data['potentialHostnames']! as List<Object?>;
|
||||||
|
try {
|
||||||
|
potentialHostnames = List<String>.from(values);
|
||||||
|
} on TypeError {
|
||||||
|
logger.printTrace('Error parsing potentialHostnames value: $values');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _IOSCoreDeviceConnectionProperties._(
|
||||||
|
authenticationType: data['authenticationType']?.toString(),
|
||||||
|
isMobileDeviceOnly: data['isMobileDeviceOnly'] is bool? ? data['isMobileDeviceOnly'] as bool? : null,
|
||||||
|
lastConnectionDate: data['lastConnectionDate']?.toString(),
|
||||||
|
localHostnames: localHostnames,
|
||||||
|
pairingState: data['pairingState']?.toString(),
|
||||||
|
potentialHostnames: potentialHostnames,
|
||||||
|
transportType: data['transportType']?.toString(),
|
||||||
|
tunnelIPAddress: data['tunnelIPAddress']?.toString(),
|
||||||
|
tunnelState: data['tunnelState']?.toString(),
|
||||||
|
tunnelTransportProtocol: data['tunnelTransportProtocol']?.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? authenticationType;
|
||||||
|
final bool? isMobileDeviceOnly;
|
||||||
|
final String? lastConnectionDate;
|
||||||
|
final List<String>? localHostnames;
|
||||||
|
final String? pairingState;
|
||||||
|
final List<String>? potentialHostnames;
|
||||||
|
final String? transportType;
|
||||||
|
final String? tunnelIPAddress;
|
||||||
|
final String? tunnelState;
|
||||||
|
final String? tunnelTransportProtocol;
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
class IOSCoreDeviceProperties {
|
||||||
|
IOSCoreDeviceProperties._({
|
||||||
|
required this.bootedFromSnapshot,
|
||||||
|
required this.bootedSnapshotName,
|
||||||
|
required this.bootState,
|
||||||
|
required this.ddiServicesAvailable,
|
||||||
|
required this.developerModeStatus,
|
||||||
|
required this.hasInternalOSBuild,
|
||||||
|
required this.name,
|
||||||
|
required this.osBuildUpdate,
|
||||||
|
required this.osVersionNumber,
|
||||||
|
required this.rootFileSystemIsWritable,
|
||||||
|
required this.screenViewingURL,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Parse `deviceProperties` section of JSON from `devicectl list devices --json-output`
|
||||||
|
/// while it's in beta preview mode.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// "deviceProperties" : {
|
||||||
|
/// "bootedFromSnapshot" : true,
|
||||||
|
/// "bootedSnapshotName" : "com.apple.os.update-B5336980824124F599FD39FE91016493A74331B09F475250BB010B276FE2439E3DE3537349A3A957D3FF2A4B623B4ECC",
|
||||||
|
/// "bootState" : "booted",
|
||||||
|
/// "ddiServicesAvailable" : true,
|
||||||
|
/// "developerModeStatus" : "enabled",
|
||||||
|
/// "hasInternalOSBuild" : false,
|
||||||
|
/// "name" : "iPadName",
|
||||||
|
/// "osBuildUpdate" : "21A5248v",
|
||||||
|
/// "osVersionNumber" : "17.0",
|
||||||
|
/// "rootFileSystemIsWritable" : false,
|
||||||
|
/// "screenViewingURL" : "coredevice-devices:/viewDeviceByUUID?uuid=123456BB5-AEDE-7A22-B890-1234567890DD"
|
||||||
|
/// }
|
||||||
|
factory IOSCoreDeviceProperties.fromBetaJson(Map<String, Object?> data) {
|
||||||
|
return IOSCoreDeviceProperties._(
|
||||||
|
bootedFromSnapshot: data['bootedFromSnapshot'] is bool? ? data['bootedFromSnapshot'] as bool? : null,
|
||||||
|
bootedSnapshotName: data['bootedSnapshotName']?.toString(),
|
||||||
|
bootState: data['bootState']?.toString(),
|
||||||
|
ddiServicesAvailable: data['ddiServicesAvailable'] is bool? ? data['ddiServicesAvailable'] as bool? : null,
|
||||||
|
developerModeStatus: data['developerModeStatus']?.toString(),
|
||||||
|
hasInternalOSBuild: data['hasInternalOSBuild'] is bool? ? data['hasInternalOSBuild'] as bool? : null,
|
||||||
|
name: data['name']?.toString(),
|
||||||
|
osBuildUpdate: data['osBuildUpdate']?.toString(),
|
||||||
|
osVersionNumber: data['osVersionNumber']?.toString(),
|
||||||
|
rootFileSystemIsWritable: data['rootFileSystemIsWritable'] is bool? ? data['rootFileSystemIsWritable'] as bool? : null,
|
||||||
|
screenViewingURL: data['screenViewingURL']?.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool? bootedFromSnapshot;
|
||||||
|
final String? bootedSnapshotName;
|
||||||
|
final String? bootState;
|
||||||
|
final bool? ddiServicesAvailable;
|
||||||
|
final String? developerModeStatus;
|
||||||
|
final bool? hasInternalOSBuild;
|
||||||
|
final String? name;
|
||||||
|
final String? osBuildUpdate;
|
||||||
|
final String? osVersionNumber;
|
||||||
|
final bool? rootFileSystemIsWritable;
|
||||||
|
final String? screenViewingURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IOSCoreDeviceHardwareProperties {
|
||||||
|
_IOSCoreDeviceHardwareProperties._({
|
||||||
|
required this.cpuType,
|
||||||
|
required this.deviceType,
|
||||||
|
required this.ecid,
|
||||||
|
required this.hardwareModel,
|
||||||
|
required this.internalStorageCapacity,
|
||||||
|
required this.marketingName,
|
||||||
|
required this.platform,
|
||||||
|
required this.productType,
|
||||||
|
required this.serialNumber,
|
||||||
|
required this.supportedCPUTypes,
|
||||||
|
required this.supportedDeviceFamilies,
|
||||||
|
required this.thinningProductType,
|
||||||
|
required this.udid,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Parse `hardwareProperties` section of JSON from `devicectl list devices --json-output`
|
||||||
|
/// while it's in beta preview mode.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// "hardwareProperties" : {
|
||||||
|
/// "cpuType" : {
|
||||||
|
/// "name" : "arm64e",
|
||||||
|
/// "subType" : 2,
|
||||||
|
/// "type" : 16777228
|
||||||
|
/// },
|
||||||
|
/// "deviceType" : "iPad",
|
||||||
|
/// "ecid" : 12345678903408542,
|
||||||
|
/// "hardwareModel" : "J617AP",
|
||||||
|
/// "internalStorageCapacity" : 128000000000,
|
||||||
|
/// "marketingName" : "iPad Pro (11-inch) (4th generation)\"",
|
||||||
|
/// "platform" : "iOS",
|
||||||
|
/// "productType" : "iPad14,3",
|
||||||
|
/// "serialNumber" : "HC123DHCQV",
|
||||||
|
/// "supportedCPUTypes" : [
|
||||||
|
/// {
|
||||||
|
/// "name" : "arm64e",
|
||||||
|
/// "subType" : 2,
|
||||||
|
/// "type" : 16777228
|
||||||
|
/// },
|
||||||
|
/// {
|
||||||
|
/// "name" : "arm64",
|
||||||
|
/// "subType" : 0,
|
||||||
|
/// "type" : 16777228
|
||||||
|
/// }
|
||||||
|
/// ],
|
||||||
|
/// "supportedDeviceFamilies" : [
|
||||||
|
/// 1,
|
||||||
|
/// 2
|
||||||
|
/// ],
|
||||||
|
/// "thinningProductType" : "iPad14,3-A",
|
||||||
|
/// "udid" : "00001234-0001234A3C03401E"
|
||||||
|
/// }
|
||||||
|
factory _IOSCoreDeviceHardwareProperties.fromBetaJson(
|
||||||
|
Map<String, Object?> data, {
|
||||||
|
required Logger logger,
|
||||||
|
}) {
|
||||||
|
_IOSCoreDeviceCPUType? cpuType;
|
||||||
|
if (data['cpuType'] is Map<String, Object?>) {
|
||||||
|
cpuType = _IOSCoreDeviceCPUType.fromBetaJson(data['cpuType']! as Map<String, Object?>);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<_IOSCoreDeviceCPUType>? supportedCPUTypes;
|
||||||
|
if (data['supportedCPUTypes'] is List<Object?>) {
|
||||||
|
final List<Object?> values = data['supportedCPUTypes']! as List<Object?>;
|
||||||
|
final List<_IOSCoreDeviceCPUType> cpuTypes = <_IOSCoreDeviceCPUType>[];
|
||||||
|
for (final Object? cpuTypeData in values) {
|
||||||
|
if (cpuTypeData is Map<String, Object?>) {
|
||||||
|
cpuTypes.add(_IOSCoreDeviceCPUType.fromBetaJson(cpuTypeData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
supportedCPUTypes = cpuTypes;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<int>? supportedDeviceFamilies;
|
||||||
|
if (data['supportedDeviceFamilies'] is List<Object?>) {
|
||||||
|
final List<Object?> values = data['supportedDeviceFamilies']! as List<Object?>;
|
||||||
|
try {
|
||||||
|
supportedDeviceFamilies = List<int>.from(values);
|
||||||
|
} on TypeError {
|
||||||
|
logger.printTrace('Error parsing supportedDeviceFamilies value: $values');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _IOSCoreDeviceHardwareProperties._(
|
||||||
|
cpuType: cpuType,
|
||||||
|
deviceType: data['deviceType']?.toString(),
|
||||||
|
ecid: data['ecid'] is int? ? data['ecid'] as int? : null,
|
||||||
|
hardwareModel: data['hardwareModel']?.toString(),
|
||||||
|
internalStorageCapacity: data['internalStorageCapacity'] is int? ? data['internalStorageCapacity'] as int? : null,
|
||||||
|
marketingName: data['marketingName']?.toString(),
|
||||||
|
platform: data['platform']?.toString(),
|
||||||
|
productType: data['productType']?.toString(),
|
||||||
|
serialNumber: data['serialNumber']?.toString(),
|
||||||
|
supportedCPUTypes: supportedCPUTypes,
|
||||||
|
supportedDeviceFamilies: supportedDeviceFamilies,
|
||||||
|
thinningProductType: data['thinningProductType']?.toString(),
|
||||||
|
udid: data['udid']?.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final _IOSCoreDeviceCPUType? cpuType;
|
||||||
|
final String? deviceType;
|
||||||
|
final int? ecid;
|
||||||
|
final String? hardwareModel;
|
||||||
|
final int? internalStorageCapacity;
|
||||||
|
final String? marketingName;
|
||||||
|
final String? platform;
|
||||||
|
final String? productType;
|
||||||
|
final String? serialNumber;
|
||||||
|
final List<_IOSCoreDeviceCPUType>? supportedCPUTypes;
|
||||||
|
final List<int>? supportedDeviceFamilies;
|
||||||
|
final String? thinningProductType;
|
||||||
|
final String? udid;
|
||||||
|
}
|
||||||
|
|
||||||
|
class _IOSCoreDeviceCPUType {
|
||||||
|
_IOSCoreDeviceCPUType._({
|
||||||
|
this.name,
|
||||||
|
this.subType,
|
||||||
|
this.cpuType,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Parse `hardwareProperties.cpuType` and `hardwareProperties.supportedCPUTypes`
|
||||||
|
/// sections of JSON from `devicectl list devices --json-output` while it's in beta preview mode.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// "cpuType" : {
|
||||||
|
/// "name" : "arm64e",
|
||||||
|
/// "subType" : 2,
|
||||||
|
/// "type" : 16777228
|
||||||
|
/// }
|
||||||
|
factory _IOSCoreDeviceCPUType.fromBetaJson(Map<String, Object?> data) {
|
||||||
|
return _IOSCoreDeviceCPUType._(
|
||||||
|
name: data['name']?.toString(),
|
||||||
|
subType: data['subType'] is int? ? data['subType'] as int? : null,
|
||||||
|
cpuType: data['type'] is int? ? data['type'] as int? : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final String? name;
|
||||||
|
final int? subType;
|
||||||
|
final int? cpuType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
class IOSCoreDeviceInstalledApp {
|
||||||
|
IOSCoreDeviceInstalledApp._({
|
||||||
|
required this.appClip,
|
||||||
|
required this.builtByDeveloper,
|
||||||
|
required this.bundleIdentifier,
|
||||||
|
required this.bundleVersion,
|
||||||
|
required this.defaultApp,
|
||||||
|
required this.hidden,
|
||||||
|
required this.internalApp,
|
||||||
|
required this.name,
|
||||||
|
required this.removable,
|
||||||
|
required this.url,
|
||||||
|
required this.version,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Parse JSON from `devicectl device info apps --json-output` while it's in
|
||||||
|
/// beta preview mode.
|
||||||
|
///
|
||||||
|
/// Example:
|
||||||
|
/// {
|
||||||
|
/// "appClip" : false,
|
||||||
|
/// "builtByDeveloper" : true,
|
||||||
|
/// "bundleIdentifier" : "com.example.flutterApp",
|
||||||
|
/// "bundleVersion" : "1",
|
||||||
|
/// "defaultApp" : false,
|
||||||
|
/// "hidden" : false,
|
||||||
|
/// "internalApp" : false,
|
||||||
|
/// "name" : "Flutter App",
|
||||||
|
/// "removable" : true,
|
||||||
|
/// "url" : "file:///private/var/containers/Bundle/Application/12345E6A-7F89-0C12-345E-F6A7E890CFF1/Runner.app/",
|
||||||
|
/// "version" : "1.0.0"
|
||||||
|
/// }
|
||||||
|
factory IOSCoreDeviceInstalledApp.fromBetaJson(Map<String, Object?> data) {
|
||||||
|
return IOSCoreDeviceInstalledApp._(
|
||||||
|
appClip: data['appClip'] is bool? ? data['appClip'] as bool? : null,
|
||||||
|
builtByDeveloper: data['builtByDeveloper'] is bool? ? data['builtByDeveloper'] as bool? : null,
|
||||||
|
bundleIdentifier: data['bundleIdentifier']?.toString(),
|
||||||
|
bundleVersion: data['bundleVersion']?.toString(),
|
||||||
|
defaultApp: data['defaultApp'] is bool? ? data['defaultApp'] as bool? : null,
|
||||||
|
hidden: data['hidden'] is bool? ? data['hidden'] as bool? : null,
|
||||||
|
internalApp: data['internalApp'] is bool? ? data['internalApp'] as bool? : null,
|
||||||
|
name: data['name']?.toString(),
|
||||||
|
removable: data['removable'] is bool? ? data['removable'] as bool? : null,
|
||||||
|
url: data['url']?.toString(),
|
||||||
|
version: data['version']?.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool? appClip;
|
||||||
|
final bool? builtByDeveloper;
|
||||||
|
final String? bundleIdentifier;
|
||||||
|
final String? bundleVersion;
|
||||||
|
final bool? defaultApp;
|
||||||
|
final bool? hidden;
|
||||||
|
final bool? internalApp;
|
||||||
|
final String? name;
|
||||||
|
final bool? removable;
|
||||||
|
final String? url;
|
||||||
|
final String? version;
|
||||||
|
}
|
@ -15,6 +15,7 @@ import '../base/io.dart';
|
|||||||
import '../base/logger.dart';
|
import '../base/logger.dart';
|
||||||
import '../base/os.dart';
|
import '../base/os.dart';
|
||||||
import '../base/platform.dart';
|
import '../base/platform.dart';
|
||||||
|
import '../base/process.dart';
|
||||||
import '../base/utils.dart';
|
import '../base/utils.dart';
|
||||||
import '../base/version.dart';
|
import '../base/version.dart';
|
||||||
import '../build_info.dart';
|
import '../build_info.dart';
|
||||||
@ -28,10 +29,13 @@ import '../project.dart';
|
|||||||
import '../protocol_discovery.dart';
|
import '../protocol_discovery.dart';
|
||||||
import '../vmservice.dart';
|
import '../vmservice.dart';
|
||||||
import 'application_package.dart';
|
import 'application_package.dart';
|
||||||
|
import 'core_devices.dart';
|
||||||
import 'ios_deploy.dart';
|
import 'ios_deploy.dart';
|
||||||
import 'ios_workflow.dart';
|
import 'ios_workflow.dart';
|
||||||
import 'iproxy.dart';
|
import 'iproxy.dart';
|
||||||
import 'mac.dart';
|
import 'mac.dart';
|
||||||
|
import 'xcode_debug.dart';
|
||||||
|
import 'xcodeproj.dart';
|
||||||
|
|
||||||
class IOSDevices extends PollingDeviceDiscovery {
|
class IOSDevices extends PollingDeviceDiscovery {
|
||||||
IOSDevices({
|
IOSDevices({
|
||||||
@ -263,16 +267,21 @@ class IOSDevice extends Device {
|
|||||||
required this.connectionInterface,
|
required this.connectionInterface,
|
||||||
required this.isConnected,
|
required this.isConnected,
|
||||||
required this.devModeEnabled,
|
required this.devModeEnabled,
|
||||||
|
required this.isCoreDevice,
|
||||||
String? sdkVersion,
|
String? sdkVersion,
|
||||||
required Platform platform,
|
required Platform platform,
|
||||||
required IOSDeploy iosDeploy,
|
required IOSDeploy iosDeploy,
|
||||||
required IMobileDevice iMobileDevice,
|
required IMobileDevice iMobileDevice,
|
||||||
|
required IOSCoreDeviceControl coreDeviceControl,
|
||||||
|
required XcodeDebug xcodeDebug,
|
||||||
required IProxy iProxy,
|
required IProxy iProxy,
|
||||||
required Logger logger,
|
required Logger logger,
|
||||||
})
|
})
|
||||||
: _sdkVersion = sdkVersion,
|
: _sdkVersion = sdkVersion,
|
||||||
_iosDeploy = iosDeploy,
|
_iosDeploy = iosDeploy,
|
||||||
_iMobileDevice = iMobileDevice,
|
_iMobileDevice = iMobileDevice,
|
||||||
|
_coreDeviceControl = coreDeviceControl,
|
||||||
|
_xcodeDebug = xcodeDebug,
|
||||||
_iproxy = iProxy,
|
_iproxy = iProxy,
|
||||||
_fileSystem = fileSystem,
|
_fileSystem = fileSystem,
|
||||||
_logger = logger,
|
_logger = logger,
|
||||||
@ -294,6 +303,8 @@ class IOSDevice extends Device {
|
|||||||
final Logger _logger;
|
final Logger _logger;
|
||||||
final Platform _platform;
|
final Platform _platform;
|
||||||
final IMobileDevice _iMobileDevice;
|
final IMobileDevice _iMobileDevice;
|
||||||
|
final IOSCoreDeviceControl _coreDeviceControl;
|
||||||
|
final XcodeDebug _xcodeDebug;
|
||||||
final IProxy _iproxy;
|
final IProxy _iproxy;
|
||||||
|
|
||||||
Version? get sdkVersion {
|
Version? get sdkVersion {
|
||||||
@ -324,6 +335,10 @@ class IOSDevice extends Device {
|
|||||||
@override
|
@override
|
||||||
bool isConnected;
|
bool isConnected;
|
||||||
|
|
||||||
|
/// CoreDevice is a device connectivity stack introduced in Xcode 15. Devices
|
||||||
|
/// with iOS 17 or greater are CoreDevices.
|
||||||
|
final bool isCoreDevice;
|
||||||
|
|
||||||
final Map<IOSApp?, DeviceLogReader> _logReaders = <IOSApp?, DeviceLogReader>{};
|
final Map<IOSApp?, DeviceLogReader> _logReaders = <IOSApp?, DeviceLogReader>{};
|
||||||
|
|
||||||
DevicePortForwarder? _portForwarder;
|
DevicePortForwarder? _portForwarder;
|
||||||
@ -349,10 +364,17 @@ class IOSDevice extends Device {
|
|||||||
}) async {
|
}) async {
|
||||||
bool result;
|
bool result;
|
||||||
try {
|
try {
|
||||||
result = await _iosDeploy.isAppInstalled(
|
if (isCoreDevice) {
|
||||||
bundleId: app.id,
|
result = await _coreDeviceControl.isAppInstalled(
|
||||||
deviceId: id,
|
bundleId: app.id,
|
||||||
);
|
deviceId: id,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
result = await _iosDeploy.isAppInstalled(
|
||||||
|
bundleId: app.id,
|
||||||
|
deviceId: id,
|
||||||
|
);
|
||||||
|
}
|
||||||
} on ProcessException catch (e) {
|
} on ProcessException catch (e) {
|
||||||
_logger.printError(e.message);
|
_logger.printError(e.message);
|
||||||
return false;
|
return false;
|
||||||
@ -376,13 +398,20 @@ class IOSDevice extends Device {
|
|||||||
|
|
||||||
int installationResult;
|
int installationResult;
|
||||||
try {
|
try {
|
||||||
installationResult = await _iosDeploy.installApp(
|
if (isCoreDevice) {
|
||||||
deviceId: id,
|
installationResult = await _coreDeviceControl.installApp(
|
||||||
bundlePath: bundle.path,
|
deviceId: id,
|
||||||
appDeltaDirectory: app.appDeltaDirectory,
|
bundlePath: bundle.path,
|
||||||
launchArguments: <String>[],
|
) ? 0 : 1;
|
||||||
interfaceType: connectionInterface,
|
} else {
|
||||||
);
|
installationResult = await _iosDeploy.installApp(
|
||||||
|
deviceId: id,
|
||||||
|
bundlePath: bundle.path,
|
||||||
|
appDeltaDirectory: app.appDeltaDirectory,
|
||||||
|
launchArguments: <String>[],
|
||||||
|
interfaceType: connectionInterface,
|
||||||
|
);
|
||||||
|
}
|
||||||
} on ProcessException catch (e) {
|
} on ProcessException catch (e) {
|
||||||
_logger.printError(e.message);
|
_logger.printError(e.message);
|
||||||
return false;
|
return false;
|
||||||
@ -404,10 +433,17 @@ class IOSDevice extends Device {
|
|||||||
}) async {
|
}) async {
|
||||||
int uninstallationResult;
|
int uninstallationResult;
|
||||||
try {
|
try {
|
||||||
uninstallationResult = await _iosDeploy.uninstallApp(
|
if (isCoreDevice) {
|
||||||
deviceId: id,
|
uninstallationResult = await _coreDeviceControl.uninstallApp(
|
||||||
bundleId: app.id,
|
deviceId: id,
|
||||||
);
|
bundleId: app.id,
|
||||||
|
) ? 0 : 1;
|
||||||
|
} else {
|
||||||
|
uninstallationResult = await _iosDeploy.uninstallApp(
|
||||||
|
deviceId: id,
|
||||||
|
bundleId: app.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
} on ProcessException catch (e) {
|
} on ProcessException catch (e) {
|
||||||
_logger.printError(e.message);
|
_logger.printError(e.message);
|
||||||
return false;
|
return false;
|
||||||
@ -434,6 +470,7 @@ class IOSDevice extends Device {
|
|||||||
bool ipv6 = false,
|
bool ipv6 = false,
|
||||||
String? userIdentifier,
|
String? userIdentifier,
|
||||||
@visibleForTesting Duration? discoveryTimeout,
|
@visibleForTesting Duration? discoveryTimeout,
|
||||||
|
@visibleForTesting ShutdownHooks? shutdownHooks,
|
||||||
}) async {
|
}) async {
|
||||||
String? packageId;
|
String? packageId;
|
||||||
if (isWirelesslyConnected &&
|
if (isWirelesslyConnected &&
|
||||||
@ -441,6 +478,18 @@ class IOSDevice extends Device {
|
|||||||
debuggingOptions.disablePortPublication) {
|
debuggingOptions.disablePortPublication) {
|
||||||
throwToolExit('Cannot start app on wirelessly tethered iOS device. Try running again with the --publish-port flag');
|
throwToolExit('Cannot start app on wirelessly tethered iOS device. Try running again with the --publish-port flag');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(vashworth): Remove after Xcode 15 and iOS 17 are in CI (https://github.com/flutter/flutter/issues/132128)
|
||||||
|
// XcodeDebug workflow is used for CoreDevices (iOS 17+ and Xcode 15+).
|
||||||
|
// Force the use of XcodeDebug workflow in CI to test from older versions
|
||||||
|
// since devicelab has not yet been updated to iOS 17 and Xcode 15.
|
||||||
|
bool forceXcodeDebugWorkflow = false;
|
||||||
|
if (debuggingOptions.usingCISystem &&
|
||||||
|
debuggingOptions.debuggingEnabled &&
|
||||||
|
_platform.environment['FORCE_XCODE_DEBUG']?.toLowerCase() == 'true') {
|
||||||
|
forceXcodeDebugWorkflow = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!prebuiltApplication) {
|
if (!prebuiltApplication) {
|
||||||
_logger.printTrace('Building ${package.name} for $id');
|
_logger.printTrace('Building ${package.name} for $id');
|
||||||
|
|
||||||
@ -451,6 +500,7 @@ class IOSDevice extends Device {
|
|||||||
targetOverride: mainPath,
|
targetOverride: mainPath,
|
||||||
activeArch: cpuArchitecture,
|
activeArch: cpuArchitecture,
|
||||||
deviceID: id,
|
deviceID: id,
|
||||||
|
isCoreDevice: isCoreDevice || forceXcodeDebugWorkflow,
|
||||||
);
|
);
|
||||||
if (!buildResult.success) {
|
if (!buildResult.success) {
|
||||||
_logger.printError('Could not build the precompiled application for the device.');
|
_logger.printError('Could not build the precompiled application for the device.');
|
||||||
@ -477,6 +527,7 @@ class IOSDevice extends Device {
|
|||||||
platformArgs,
|
platformArgs,
|
||||||
ipv6: ipv6,
|
ipv6: ipv6,
|
||||||
interfaceType: connectionInterface,
|
interfaceType: connectionInterface,
|
||||||
|
isCoreDevice: isCoreDevice,
|
||||||
);
|
);
|
||||||
Status startAppStatus = _logger.startProgress(
|
Status startAppStatus = _logger.startProgress(
|
||||||
'Installing and launching...',
|
'Installing and launching...',
|
||||||
@ -516,7 +567,16 @@ class IOSDevice extends Device {
|
|||||||
logger: _logger,
|
logger: _logger,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (iosDeployDebugger == null) {
|
|
||||||
|
if (isCoreDevice || forceXcodeDebugWorkflow) {
|
||||||
|
installationResult = await _startAppOnCoreDevice(
|
||||||
|
debuggingOptions: debuggingOptions,
|
||||||
|
package: package,
|
||||||
|
launchArguments: launchArguments,
|
||||||
|
discoveryTimeout: discoveryTimeout,
|
||||||
|
shutdownHooks: shutdownHooks ?? globals.shutdownHooks,
|
||||||
|
) ? 0 : 1;
|
||||||
|
} else if (iosDeployDebugger == null) {
|
||||||
installationResult = await _iosDeploy.launchApp(
|
installationResult = await _iosDeploy.launchApp(
|
||||||
deviceId: id,
|
deviceId: id,
|
||||||
bundlePath: bundle.path,
|
bundlePath: bundle.path,
|
||||||
@ -543,10 +603,26 @@ class IOSDevice extends Device {
|
|||||||
|
|
||||||
_logger.printTrace('Application launched on the device. Waiting for Dart VM Service url.');
|
_logger.printTrace('Application launched on the device. Waiting for Dart VM Service url.');
|
||||||
|
|
||||||
final int defaultTimeout = isWirelesslyConnected ? 45 : 30;
|
final int defaultTimeout;
|
||||||
|
if ((isCoreDevice || forceXcodeDebugWorkflow) && debuggingOptions.debuggingEnabled) {
|
||||||
|
// Core devices with debugging enabled takes longer because this
|
||||||
|
// includes time to install and launch the app on the device.
|
||||||
|
defaultTimeout = isWirelesslyConnected ? 75 : 60;
|
||||||
|
} else if (isWirelesslyConnected) {
|
||||||
|
defaultTimeout = 45;
|
||||||
|
} else {
|
||||||
|
defaultTimeout = 30;
|
||||||
|
}
|
||||||
|
|
||||||
final Timer timer = Timer(discoveryTimeout ?? Duration(seconds: defaultTimeout), () {
|
final Timer timer = Timer(discoveryTimeout ?? Duration(seconds: defaultTimeout), () {
|
||||||
_logger.printError('The Dart VM Service was not discovered after $defaultTimeout seconds. This is taking much longer than expected...');
|
_logger.printError('The Dart VM Service was not discovered after $defaultTimeout seconds. This is taking much longer than expected...');
|
||||||
|
if (isCoreDevice && debuggingOptions.debuggingEnabled) {
|
||||||
|
_logger.printError(
|
||||||
|
'Open the Xcode window the project is opened in to ensure the app '
|
||||||
|
'is running. If the app is not running, try selecting "Product > Run" '
|
||||||
|
'to fix the problem.',
|
||||||
|
);
|
||||||
|
}
|
||||||
// If debugging with a wireless device and the timeout is reached, remind the
|
// If debugging with a wireless device and the timeout is reached, remind the
|
||||||
// user to allow local network permissions.
|
// user to allow local network permissions.
|
||||||
if (isWirelesslyConnected) {
|
if (isWirelesslyConnected) {
|
||||||
@ -564,37 +640,71 @@ class IOSDevice extends Device {
|
|||||||
|
|
||||||
Uri? localUri;
|
Uri? localUri;
|
||||||
if (isWirelesslyConnected) {
|
if (isWirelesslyConnected) {
|
||||||
// Wait for Dart VM Service to start up.
|
// When using a CoreDevice, device logs are unavailable and therefore
|
||||||
final Uri? serviceURL = await vmServiceDiscovery?.uri;
|
// cannot be used to get the Dart VM url. Instead, get the Dart VM
|
||||||
if (serviceURL == null) {
|
// Service by finding services matching the app bundle id and the
|
||||||
await iosDeployDebugger?.stopAndDumpBacktrace();
|
// device name.
|
||||||
await dispose();
|
//
|
||||||
return LaunchResult.failed();
|
// If not using a CoreDevice, wait for the Dart VM url to be discovered
|
||||||
}
|
// via logs and then get the Dart VM Service by finding services matching
|
||||||
|
// the app bundle id and the Dart VM port.
|
||||||
// If Dart VM Service URL with the device IP is not found within 5 seconds,
|
//
|
||||||
// change the status message to prompt users to click Allow. Wait 5 seconds because it
|
// Then in both cases, get the device IP from the Dart VM Service to
|
||||||
// should only show this message if they have not already approved the permissions.
|
// construct the Dart VM url using the device IP as the host.
|
||||||
// MDnsVmServiceDiscovery usually takes less than 5 seconds to find it.
|
if (isCoreDevice) {
|
||||||
final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () {
|
localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
|
||||||
startAppStatus.stop();
|
packageId,
|
||||||
startAppStatus = _logger.startProgress(
|
this,
|
||||||
'Waiting for approval of local network permissions...',
|
usesIpv6: ipv6,
|
||||||
|
useDeviceIPAsHost: true,
|
||||||
);
|
);
|
||||||
});
|
} else {
|
||||||
|
// Wait for Dart VM Service to start up.
|
||||||
|
final Uri? serviceURL = await vmServiceDiscovery?.uri;
|
||||||
|
if (serviceURL == null) {
|
||||||
|
await iosDeployDebugger?.stopAndDumpBacktrace();
|
||||||
|
await dispose();
|
||||||
|
return LaunchResult.failed();
|
||||||
|
}
|
||||||
|
|
||||||
// Get Dart VM Service URL with the device IP as the host.
|
// If Dart VM Service URL with the device IP is not found within 5 seconds,
|
||||||
localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
|
// change the status message to prompt users to click Allow. Wait 5 seconds because it
|
||||||
packageId,
|
// should only show this message if they have not already approved the permissions.
|
||||||
this,
|
// MDnsVmServiceDiscovery usually takes less than 5 seconds to find it.
|
||||||
usesIpv6: ipv6,
|
final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () {
|
||||||
deviceVmservicePort: serviceURL.port,
|
startAppStatus.stop();
|
||||||
useDeviceIPAsHost: true,
|
startAppStatus = _logger.startProgress(
|
||||||
);
|
'Waiting for approval of local network permissions...',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
mDNSLookupTimer.cancel();
|
// Get Dart VM Service URL with the device IP as the host.
|
||||||
|
localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
|
||||||
|
packageId,
|
||||||
|
this,
|
||||||
|
usesIpv6: ipv6,
|
||||||
|
deviceVmservicePort: serviceURL.port,
|
||||||
|
useDeviceIPAsHost: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
mDNSLookupTimer.cancel();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
localUri = await vmServiceDiscovery?.uri;
|
if (isCoreDevice && vmServiceDiscovery != null) {
|
||||||
|
// When searching for the Dart VM url, search for it via ProtocolDiscovery
|
||||||
|
// (device logs) and mDNS simultaneously, since both can be flaky at times.
|
||||||
|
final Future<Uri?> vmUrlFromMDns = MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
|
||||||
|
packageId,
|
||||||
|
this,
|
||||||
|
usesIpv6: ipv6,
|
||||||
|
);
|
||||||
|
final Future<Uri?> vmUrlFromLogs = vmServiceDiscovery.uri;
|
||||||
|
localUri = await Future.any(
|
||||||
|
<Future<Uri?>>[vmUrlFromMDns, vmUrlFromLogs]
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
localUri = await vmServiceDiscovery?.uri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
if (localUri == null) {
|
if (localUri == null) {
|
||||||
@ -613,6 +723,110 @@ class IOSDevice extends Device {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starting with Xcode 15 and iOS 17, `ios-deploy` stopped working due to
|
||||||
|
/// the new CoreDevice connectivity stack. Previously, `ios-deploy` was used
|
||||||
|
/// to install the app, launch the app, and start `debugserver`.
|
||||||
|
/// Xcode 15 introduced a new command line tool called `devicectl` that
|
||||||
|
/// includes much of the functionality supplied by `ios-deploy`. However,
|
||||||
|
/// `devicectl` lacks the ability to start a `debugserver` and therefore `ptrace`, which are needed
|
||||||
|
/// for debug mode due to using a JIT Dart VM.
|
||||||
|
///
|
||||||
|
/// Therefore, when starting an app on a CoreDevice, use `devicectl` when
|
||||||
|
/// debugging is not enabled. Otherwise, use Xcode automation.
|
||||||
|
Future<bool> _startAppOnCoreDevice({
|
||||||
|
required DebuggingOptions debuggingOptions,
|
||||||
|
required IOSApp package,
|
||||||
|
required List<String> launchArguments,
|
||||||
|
required ShutdownHooks shutdownHooks,
|
||||||
|
@visibleForTesting Duration? discoveryTimeout,
|
||||||
|
}) async {
|
||||||
|
if (!debuggingOptions.debuggingEnabled) {
|
||||||
|
// Release mode
|
||||||
|
|
||||||
|
// Install app to device
|
||||||
|
final bool installSuccess = await _coreDeviceControl.installApp(
|
||||||
|
deviceId: id,
|
||||||
|
bundlePath: package.deviceBundlePath,
|
||||||
|
);
|
||||||
|
if (!installSuccess) {
|
||||||
|
return installSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Launch app to device
|
||||||
|
final bool launchSuccess = await _coreDeviceControl.launchApp(
|
||||||
|
deviceId: id,
|
||||||
|
bundleId: package.id,
|
||||||
|
launchArguments: launchArguments,
|
||||||
|
);
|
||||||
|
|
||||||
|
return launchSuccess;
|
||||||
|
} else {
|
||||||
|
_logger.printStatus(
|
||||||
|
'You may be prompted to give access to control Xcode. Flutter uses Xcode '
|
||||||
|
'to run your app. If access is not allowed, you can change this through '
|
||||||
|
'your Settings > Privacy & Security > Automation.',
|
||||||
|
);
|
||||||
|
final int launchTimeout = isWirelesslyConnected ? 45 : 30;
|
||||||
|
final Timer timer = Timer(discoveryTimeout ?? Duration(seconds: launchTimeout), () {
|
||||||
|
_logger.printError(
|
||||||
|
'Xcode is taking longer than expected to start debugging the app. '
|
||||||
|
'Ensure the project is opened in Xcode.',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
XcodeDebugProject debugProject;
|
||||||
|
|
||||||
|
if (package is PrebuiltIOSApp) {
|
||||||
|
debugProject = await _xcodeDebug.createXcodeProjectWithCustomBundle(
|
||||||
|
package.deviceBundlePath,
|
||||||
|
templateRenderer: globals.templateRenderer,
|
||||||
|
verboseLogging: _logger.isVerbose,
|
||||||
|
);
|
||||||
|
} else if (package is BuildableIOSApp) {
|
||||||
|
final IosProject project = package.project;
|
||||||
|
final XcodeProjectInfo? projectInfo = await project.projectInfo();
|
||||||
|
if (projectInfo == null) {
|
||||||
|
globals.printError('Xcode project not found.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (project.xcodeWorkspace == null) {
|
||||||
|
globals.printError('Unable to get Xcode workspace.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final String? scheme = projectInfo.schemeFor(debuggingOptions.buildInfo);
|
||||||
|
if (scheme == null) {
|
||||||
|
projectInfo.reportFlavorNotFoundAndExit();
|
||||||
|
}
|
||||||
|
|
||||||
|
debugProject = XcodeDebugProject(
|
||||||
|
scheme: scheme,
|
||||||
|
xcodeProject: project.xcodeProject,
|
||||||
|
xcodeWorkspace: project.xcodeWorkspace!,
|
||||||
|
verboseLogging: _logger.isVerbose,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// This should not happen. Currently, only PrebuiltIOSApp and
|
||||||
|
// BuildableIOSApp extend from IOSApp.
|
||||||
|
_logger.printError('IOSApp type ${package.runtimeType} is not recognized.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool debugSuccess = await _xcodeDebug.debugApp(
|
||||||
|
project: debugProject,
|
||||||
|
deviceId: id,
|
||||||
|
launchArguments:launchArguments,
|
||||||
|
);
|
||||||
|
timer.cancel();
|
||||||
|
|
||||||
|
// Kill Xcode on shutdown when running from CI
|
||||||
|
if (debuggingOptions.usingCISystem) {
|
||||||
|
shutdownHooks.addShutdownHook(() => _xcodeDebug.exit(force: true));
|
||||||
|
}
|
||||||
|
|
||||||
|
return debugSuccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<bool> stopApp(
|
Future<bool> stopApp(
|
||||||
ApplicationPackage? app, {
|
ApplicationPackage? app, {
|
||||||
@ -623,6 +837,9 @@ class IOSDevice extends Device {
|
|||||||
if (deployDebugger != null && deployDebugger.debuggerAttached) {
|
if (deployDebugger != null && deployDebugger.debuggerAttached) {
|
||||||
return deployDebugger.exit();
|
return deployDebugger.exit();
|
||||||
}
|
}
|
||||||
|
if (_xcodeDebug.debugStarted) {
|
||||||
|
return _xcodeDebug.exit();
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -669,7 +886,14 @@ class IOSDevice extends Device {
|
|||||||
void clearLogs() { }
|
void clearLogs() { }
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool get supportsScreenshot => _iMobileDevice.isInstalled;
|
bool get supportsScreenshot {
|
||||||
|
if (isCoreDevice) {
|
||||||
|
// `idevicescreenshot` stopped working with iOS 17 / Xcode 15
|
||||||
|
// (https://github.com/flutter/flutter/issues/128598).
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return _iMobileDevice.isInstalled;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> takeScreenshot(File outputFile) async {
|
Future<void> takeScreenshot(File outputFile) async {
|
||||||
@ -757,14 +981,18 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||||||
this._majorSdkVersion,
|
this._majorSdkVersion,
|
||||||
this._deviceId,
|
this._deviceId,
|
||||||
this.name,
|
this.name,
|
||||||
|
this._isWirelesslyConnected,
|
||||||
|
this._isCoreDevice,
|
||||||
String appName,
|
String appName,
|
||||||
bool usingCISystem,
|
bool usingCISystem, {
|
||||||
) : // Match for lines for the runner in syslog.
|
bool forceXcodeDebug = false,
|
||||||
|
}) : // Match for lines for the runner in syslog.
|
||||||
//
|
//
|
||||||
// iOS 9 format: Runner[297] <Notice>:
|
// iOS 9 format: Runner[297] <Notice>:
|
||||||
// iOS 10 format: Runner(Flutter)[297] <Notice>:
|
// iOS 10 format: Runner(Flutter)[297] <Notice>:
|
||||||
_runnerLineRegex = RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: '),
|
_runnerLineRegex = RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: '),
|
||||||
_usingCISystem = usingCISystem;
|
_usingCISystem = usingCISystem,
|
||||||
|
_forceXcodeDebug = forceXcodeDebug;
|
||||||
|
|
||||||
/// Create a new [IOSDeviceLogReader].
|
/// Create a new [IOSDeviceLogReader].
|
||||||
factory IOSDeviceLogReader.create({
|
factory IOSDeviceLogReader.create({
|
||||||
@ -779,8 +1007,11 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||||||
device.majorSdkVersion,
|
device.majorSdkVersion,
|
||||||
device.id,
|
device.id,
|
||||||
device.name,
|
device.name,
|
||||||
|
device.isWirelesslyConnected,
|
||||||
|
device.isCoreDevice,
|
||||||
appName,
|
appName,
|
||||||
usingCISystem,
|
usingCISystem,
|
||||||
|
forceXcodeDebug: device._platform.environment['FORCE_XCODE_DEBUG']?.toLowerCase() == 'true',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -790,6 +1021,8 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||||||
bool useSyslog = true,
|
bool useSyslog = true,
|
||||||
bool usingCISystem = false,
|
bool usingCISystem = false,
|
||||||
int? majorSdkVersion,
|
int? majorSdkVersion,
|
||||||
|
bool isWirelesslyConnected = false,
|
||||||
|
bool isCoreDevice = false,
|
||||||
}) {
|
}) {
|
||||||
final int sdkVersion;
|
final int sdkVersion;
|
||||||
if (majorSdkVersion != null) {
|
if (majorSdkVersion != null) {
|
||||||
@ -798,16 +1031,22 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||||||
sdkVersion = useSyslog ? 12 : 13;
|
sdkVersion = useSyslog ? 12 : 13;
|
||||||
}
|
}
|
||||||
return IOSDeviceLogReader._(
|
return IOSDeviceLogReader._(
|
||||||
iMobileDevice, sdkVersion, '1234', 'test', 'Runner', usingCISystem);
|
iMobileDevice, sdkVersion, '1234', 'test', isWirelesslyConnected, isCoreDevice, 'Runner', usingCISystem);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
final String name;
|
final String name;
|
||||||
final int _majorSdkVersion;
|
final int _majorSdkVersion;
|
||||||
final String _deviceId;
|
final String _deviceId;
|
||||||
|
final bool _isWirelesslyConnected;
|
||||||
|
final bool _isCoreDevice;
|
||||||
final IMobileDevice _iMobileDevice;
|
final IMobileDevice _iMobileDevice;
|
||||||
final bool _usingCISystem;
|
final bool _usingCISystem;
|
||||||
|
|
||||||
|
// TODO(vashworth): Remove after Xcode 15 and iOS 17 are in CI (https://github.com/flutter/flutter/issues/132128)
|
||||||
|
/// Whether XcodeDebug workflow is being forced.
|
||||||
|
final bool _forceXcodeDebug;
|
||||||
|
|
||||||
// Matches a syslog line from the runner.
|
// Matches a syslog line from the runner.
|
||||||
RegExp _runnerLineRegex;
|
RegExp _runnerLineRegex;
|
||||||
|
|
||||||
@ -845,16 +1084,13 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||||||
/// is true.
|
/// is true.
|
||||||
final List<String> _streamFlutterMessages = <String>[];
|
final List<String> _streamFlutterMessages = <String>[];
|
||||||
|
|
||||||
/// When using both `idevicesyslog` and `ios-deploy`, exclude logs with the
|
/// There are three potential logging sources: `idevicesyslog`, `ios-deploy`,
|
||||||
/// "flutter:" prefix if they have already been added to the stream. This is
|
/// and Unified Logging (Dart VM). When using more than one of these logging
|
||||||
/// to prevent duplicates from being printed.
|
/// sources at a time, exclude logs with a `flutter:` prefix if they have
|
||||||
///
|
/// already been added to the stream. This is to prevent duplicates from
|
||||||
/// If a message does not have the prefix, exclude it if the message's
|
/// being printed.
|
||||||
/// source is `idevicesyslog`. This is done because `ios-deploy` and
|
|
||||||
/// `idevicesyslog` often have different prefixes on non-flutter messages
|
|
||||||
/// and are often not critical for CI tests.
|
|
||||||
bool _excludeLog(String message, IOSDeviceLogSource source) {
|
bool _excludeLog(String message, IOSDeviceLogSource source) {
|
||||||
if (!useBothLogDeviceReaders) {
|
if (!usingMultipleLoggingSources) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (message.startsWith('flutter:')) {
|
if (message.startsWith('flutter:')) {
|
||||||
@ -862,7 +1098,12 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
_streamFlutterMessages.add(message);
|
_streamFlutterMessages.add(message);
|
||||||
} else if (source == IOSDeviceLogSource.idevicesyslog) {
|
} else if (useIOSDeployLogging && source == IOSDeviceLogSource.idevicesyslog) {
|
||||||
|
// If using both `ios-deploy` and `idevicesyslog` simultaneously, exclude
|
||||||
|
// the message if its source is `idevicesyslog`. This is done because
|
||||||
|
//`ios-deploy` and `idevicesyslog` often have different prefixes, which
|
||||||
|
// makes duplicate matching difficult. Instead, exclude any non-flutter-prefixed
|
||||||
|
// `idevicesyslog` messages, which are not critical for CI tests.
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@ -887,12 +1128,114 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||||||
|
|
||||||
static const int minimumUniversalLoggingSdkVersion = 13;
|
static const int minimumUniversalLoggingSdkVersion = 13;
|
||||||
|
|
||||||
|
/// Use `idevicesyslog` to stream logs from the device when one of the
|
||||||
|
/// following criteria is met:
|
||||||
|
///
|
||||||
|
/// 1) The device is a physically attached CoreDevice.
|
||||||
|
/// 2) The device has iOS 16 or greater and it's being debugged from CI.
|
||||||
|
/// 3) The device has iOS 12 or lower.
|
||||||
|
///
|
||||||
|
/// Syslog stopped working on iOS 13 (https://github.com/flutter/flutter/issues/41133).
|
||||||
|
/// However, from at least iOS 16, it has began working again. It's unclear
|
||||||
|
/// why it started working again.
|
||||||
|
@visibleForTesting
|
||||||
|
bool get useSyslogLogging {
|
||||||
|
// When forcing XcodeDebug workflow, use `idevicesyslog`.
|
||||||
|
if (_forceXcodeDebug) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `ios-deploy` stopped working with iOS 17 / Xcode 15, so use `idevicesyslog` instead.
|
||||||
|
// However, `idevicesyslog` does not work with iOS 17 wireless devices.
|
||||||
|
if (_isCoreDevice && !_isWirelesslyConnected) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use both logs from `idevicesyslog` and `ios-deploy` when debugging from CI system
|
||||||
|
// since sometimes `ios-deploy` does not return the device logs:
|
||||||
|
// https://github.com/flutter/flutter/issues/121231
|
||||||
|
if (_usingCISystem && _majorSdkVersion >= 16) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Use the Dart VM to stream logs from the device when one of the following
|
||||||
|
/// criteria is met:
|
||||||
|
///
|
||||||
|
/// 1) The device is a CoreDevice and wirelessly connected.
|
||||||
|
/// 2) The device has iOS 13 or greater and [_iosDeployDebugger] is null or
|
||||||
|
/// the [_iosDeployDebugger] debugger is not attached.
|
||||||
|
///
|
||||||
|
/// This value may change if [_iosDeployDebugger] changes.
|
||||||
|
@visibleForTesting
|
||||||
|
bool get useUnifiedLogging {
|
||||||
|
// Can't use Unified Logging if it's not going to listen to the Dart VM.
|
||||||
|
if (!_shouldListenForUnifiedLoggingEvents) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `idevicesyslog` doesn't work on wireless devices, so use logs from Dart VM instead.
|
||||||
|
if (_isCoreDevice) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prefer the more complete logs from the attached debugger, if they are available.
|
||||||
|
if (_majorSdkVersion >= minimumUniversalLoggingSdkVersion && (_iosDeployDebugger == null || !_iosDeployDebugger!.debuggerAttached)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Determine whether to listen to the Dart VM for logging events. Returns
|
||||||
|
/// true when one of the following criteria is met:
|
||||||
|
///
|
||||||
|
/// 1) The device is a CoreDevice and wirelessly connected.
|
||||||
|
/// 2) The device has iOS 13 or greater.
|
||||||
|
bool get _shouldListenForUnifiedLoggingEvents {
|
||||||
|
// `idevicesyslog` doesn't work on wireless devices, so use logs from Dart VM instead.
|
||||||
|
if (_isCoreDevice) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_majorSdkVersion >= minimumUniversalLoggingSdkVersion) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Use `ios-deploy` to stream logs from the device when the device is not a
|
||||||
|
/// CoreDevice and has iOS 13 or greater.
|
||||||
|
@visibleForTesting
|
||||||
|
bool get useIOSDeployLogging {
|
||||||
|
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion || _isCoreDevice) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
/// Returns true when using multiple sources for streaming the device logs.
|
||||||
|
bool get usingMultipleLoggingSources {
|
||||||
|
final int numberOfSources = (useIOSDeployLogging ? 1 : 0) + (useSyslogLogging ? 1 : 0) + (useUnifiedLogging ? 1 : 0);
|
||||||
|
if (numberOfSources > 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// Listen to Dart VM for logs on iOS 13 or greater.
|
/// Listen to Dart VM for logs on iOS 13 or greater.
|
||||||
///
|
///
|
||||||
/// Only send logs to stream if [_iosDeployDebugger] is null or
|
/// Only send logs to stream if [_iosDeployDebugger] is null or
|
||||||
/// the [_iosDeployDebugger] debugger is not attached.
|
/// the [_iosDeployDebugger] debugger is not attached.
|
||||||
Future<void> _listenToUnifiedLoggingEvents(FlutterVmService connectedVmService) async {
|
Future<void> _listenToUnifiedLoggingEvents(FlutterVmService connectedVmService) async {
|
||||||
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
|
if (!_shouldListenForUnifiedLoggingEvents) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
@ -909,7 +1252,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void logMessage(vm_service.Event event) {
|
void logMessage(vm_service.Event event) {
|
||||||
if (_iosDeployDebugger != null && _iosDeployDebugger!.debuggerAttached) {
|
if (!useUnifiedLogging) {
|
||||||
// Prefer the more complete logs from the attached debugger.
|
// Prefer the more complete logs from the attached debugger.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -931,7 +1274,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||||||
/// Send messages from ios-deploy debugger stream to device log reader stream.
|
/// Send messages from ios-deploy debugger stream to device log reader stream.
|
||||||
set debuggerStream(IOSDeployDebugger? debugger) {
|
set debuggerStream(IOSDeployDebugger? debugger) {
|
||||||
// Logging is gathered from syslog on iOS earlier than 13.
|
// Logging is gathered from syslog on iOS earlier than 13.
|
||||||
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
|
if (!useIOSDeployLogging) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_iosDeployDebugger = debugger;
|
_iosDeployDebugger = debugger;
|
||||||
@ -954,22 +1297,10 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||||||
// Strip off the logging metadata (leave the category), or just echo the line.
|
// Strip off the logging metadata (leave the category), or just echo the line.
|
||||||
String _debuggerLineHandler(String line) => _debuggerLoggingRegex.firstMatch(line)?.group(1) ?? line;
|
String _debuggerLineHandler(String line) => _debuggerLoggingRegex.firstMatch(line)?.group(1) ?? line;
|
||||||
|
|
||||||
/// Use both logs from `idevicesyslog` and `ios-deploy` when debugging from CI system
|
|
||||||
/// since sometimes `ios-deploy` does not return the device logs:
|
|
||||||
/// https://github.com/flutter/flutter/issues/121231
|
|
||||||
@visibleForTesting
|
|
||||||
bool get useBothLogDeviceReaders {
|
|
||||||
return _usingCISystem && _majorSdkVersion >= 16;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Start and listen to idevicesyslog to get device logs for iOS versions
|
/// Start and listen to idevicesyslog to get device logs for iOS versions
|
||||||
/// prior to 13 or if [useBothLogDeviceReaders] is true.
|
/// prior to 13 or if [useBothLogDeviceReaders] is true.
|
||||||
void _listenToSysLog() {
|
void _listenToSysLog() {
|
||||||
// Syslog stopped working on iOS 13 (https://github.com/flutter/flutter/issues/41133).
|
if (!useSyslogLogging) {
|
||||||
// However, from at least iOS 16, it has began working again. It's unclear
|
|
||||||
// why it started working again so only use syslogs for iOS versions prior
|
|
||||||
// to 13 unless [useBothLogDeviceReaders] is true.
|
|
||||||
if (!useBothLogDeviceReaders && _majorSdkVersion >= minimumUniversalLoggingSdkVersion) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_iMobileDevice.startLogger(_deviceId).then<void>((Process process) {
|
_iMobileDevice.startLogger(_deviceId).then<void>((Process process) {
|
||||||
@ -982,7 +1313,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
|||||||
// When using both log readers, do not close the stream on exit.
|
// When using both log readers, do not close the stream on exit.
|
||||||
// This is to allow ios-deploy to be the source of authority to close
|
// This is to allow ios-deploy to be the source of authority to close
|
||||||
// the stream.
|
// the stream.
|
||||||
if (useBothLogDeviceReaders && debuggerStream != null) {
|
if (useSyslogLogging && useIOSDeployLogging && debuggerStream != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
linesController.close();
|
linesController.close();
|
||||||
|
@ -132,6 +132,7 @@ Future<XcodeBuildResult> buildXcodeProject({
|
|||||||
DarwinArch? activeArch,
|
DarwinArch? activeArch,
|
||||||
bool codesign = true,
|
bool codesign = true,
|
||||||
String? deviceID,
|
String? deviceID,
|
||||||
|
bool isCoreDevice = false,
|
||||||
bool configOnly = false,
|
bool configOnly = false,
|
||||||
XcodeBuildAction buildAction = XcodeBuildAction.build,
|
XcodeBuildAction buildAction = XcodeBuildAction.build,
|
||||||
}) async {
|
}) async {
|
||||||
@ -240,6 +241,7 @@ Future<XcodeBuildResult> buildXcodeProject({
|
|||||||
project: project,
|
project: project,
|
||||||
targetOverride: targetOverride,
|
targetOverride: targetOverride,
|
||||||
buildInfo: buildInfo,
|
buildInfo: buildInfo,
|
||||||
|
usingCoreDevice: isCoreDevice,
|
||||||
);
|
);
|
||||||
await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
|
await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
|
||||||
if (configOnly) {
|
if (configOnly) {
|
||||||
|
@ -35,6 +35,7 @@ Future<void> updateGeneratedXcodeProperties({
|
|||||||
String? targetOverride,
|
String? targetOverride,
|
||||||
bool useMacOSConfig = false,
|
bool useMacOSConfig = false,
|
||||||
String? buildDirOverride,
|
String? buildDirOverride,
|
||||||
|
bool usingCoreDevice = false,
|
||||||
}) async {
|
}) async {
|
||||||
final List<String> xcodeBuildSettings = await _xcodeBuildSettingsLines(
|
final List<String> xcodeBuildSettings = await _xcodeBuildSettingsLines(
|
||||||
project: project,
|
project: project,
|
||||||
@ -42,6 +43,7 @@ Future<void> updateGeneratedXcodeProperties({
|
|||||||
targetOverride: targetOverride,
|
targetOverride: targetOverride,
|
||||||
useMacOSConfig: useMacOSConfig,
|
useMacOSConfig: useMacOSConfig,
|
||||||
buildDirOverride: buildDirOverride,
|
buildDirOverride: buildDirOverride,
|
||||||
|
usingCoreDevice: usingCoreDevice,
|
||||||
);
|
);
|
||||||
|
|
||||||
_updateGeneratedXcodePropertiesFile(
|
_updateGeneratedXcodePropertiesFile(
|
||||||
@ -143,6 +145,7 @@ Future<List<String>> _xcodeBuildSettingsLines({
|
|||||||
String? targetOverride,
|
String? targetOverride,
|
||||||
bool useMacOSConfig = false,
|
bool useMacOSConfig = false,
|
||||||
String? buildDirOverride,
|
String? buildDirOverride,
|
||||||
|
bool usingCoreDevice = false,
|
||||||
}) async {
|
}) async {
|
||||||
final List<String> xcodeBuildSettings = <String>[];
|
final List<String> xcodeBuildSettings = <String>[];
|
||||||
|
|
||||||
@ -170,6 +173,12 @@ Future<List<String>> _xcodeBuildSettingsLines({
|
|||||||
final String buildNumber = parsedBuildNumber(manifest: project.manifest, buildInfo: buildInfo) ?? '1';
|
final String buildNumber = parsedBuildNumber(manifest: project.manifest, buildInfo: buildInfo) ?? '1';
|
||||||
xcodeBuildSettings.add('FLUTTER_BUILD_NUMBER=$buildNumber');
|
xcodeBuildSettings.add('FLUTTER_BUILD_NUMBER=$buildNumber');
|
||||||
|
|
||||||
|
// CoreDevices in debug and profile mode are launched, but not built, via Xcode.
|
||||||
|
// Set the BUILD_DIR so Xcode knows where to find the app bundle to launch.
|
||||||
|
if (usingCoreDevice && !buildInfo.isRelease) {
|
||||||
|
xcodeBuildSettings.add('BUILD_DIR=${globals.fs.path.absolute(getIosBuildDirectory())}');
|
||||||
|
}
|
||||||
|
|
||||||
final LocalEngineInfo? localEngineInfo = globals.artifacts?.localEngineInfo;
|
final LocalEngineInfo? localEngineInfo = globals.artifacts?.localEngineInfo;
|
||||||
if (localEngineInfo != null) {
|
if (localEngineInfo != null) {
|
||||||
final String engineOutPath = localEngineInfo.engineOutPath;
|
final String engineOutPath = localEngineInfo.engineOutPath;
|
||||||
|
479
packages/flutter_tools/lib/src/ios/xcode_debug.dart
Normal file
479
packages/flutter_tools/lib/src/ios/xcode_debug.dart
Normal file
@ -0,0 +1,479 @@
|
|||||||
|
// 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 'package:meta/meta.dart';
|
||||||
|
import 'package:process/process.dart';
|
||||||
|
|
||||||
|
import '../base/file_system.dart';
|
||||||
|
import '../base/io.dart';
|
||||||
|
import '../base/logger.dart';
|
||||||
|
import '../base/process.dart';
|
||||||
|
import '../base/template.dart';
|
||||||
|
import '../convert.dart';
|
||||||
|
import '../macos/xcode.dart';
|
||||||
|
import '../template.dart';
|
||||||
|
|
||||||
|
/// A class to handle interacting with Xcode via OSA (Open Scripting Architecture)
|
||||||
|
/// Scripting to debug Flutter applications.
|
||||||
|
class XcodeDebug {
|
||||||
|
XcodeDebug({
|
||||||
|
required Logger logger,
|
||||||
|
required ProcessManager processManager,
|
||||||
|
required Xcode xcode,
|
||||||
|
required FileSystem fileSystem,
|
||||||
|
}) : _logger = logger,
|
||||||
|
_processUtils = ProcessUtils(logger: logger, processManager: processManager),
|
||||||
|
_xcode = xcode,
|
||||||
|
_fileSystem = fileSystem;
|
||||||
|
|
||||||
|
final ProcessUtils _processUtils;
|
||||||
|
final Logger _logger;
|
||||||
|
final Xcode _xcode;
|
||||||
|
final FileSystem _fileSystem;
|
||||||
|
|
||||||
|
/// Process to start Xcode's debug action.
|
||||||
|
@visibleForTesting
|
||||||
|
Process? startDebugActionProcess;
|
||||||
|
|
||||||
|
/// Information about the project that is currently being debugged.
|
||||||
|
@visibleForTesting
|
||||||
|
XcodeDebugProject? currentDebuggingProject;
|
||||||
|
|
||||||
|
/// Whether the debug action has been started.
|
||||||
|
bool get debugStarted => currentDebuggingProject != null;
|
||||||
|
|
||||||
|
/// Install, launch, and start a debug session for app through Xcode interface,
|
||||||
|
/// automated by OSA scripting. First checks if the project is opened in
|
||||||
|
/// Xcode. If it isn't, open it with the `open` command.
|
||||||
|
///
|
||||||
|
/// The OSA script waits until the project is opened and the debug action
|
||||||
|
/// has started. It does not wait for the app to install, launch, or start
|
||||||
|
/// the debug session.
|
||||||
|
Future<bool> debugApp({
|
||||||
|
required XcodeDebugProject project,
|
||||||
|
required String deviceId,
|
||||||
|
required List<String> launchArguments,
|
||||||
|
}) async {
|
||||||
|
|
||||||
|
// If project is not already opened in Xcode, open it.
|
||||||
|
if (!await _isProjectOpenInXcode(project: project)) {
|
||||||
|
final bool openResult = await _openProjectInXcode(xcodeWorkspace: project.xcodeWorkspace);
|
||||||
|
if (!openResult) {
|
||||||
|
return openResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDebuggingProject = project;
|
||||||
|
StreamSubscription<String>? stdoutSubscription;
|
||||||
|
StreamSubscription<String>? stderrSubscription;
|
||||||
|
try {
|
||||||
|
startDebugActionProcess = await _processUtils.start(
|
||||||
|
<String>[
|
||||||
|
..._xcode.xcrunCommand(),
|
||||||
|
'osascript',
|
||||||
|
'-l',
|
||||||
|
'JavaScript',
|
||||||
|
_xcode.xcodeAutomationScriptPath,
|
||||||
|
'debug',
|
||||||
|
'--xcode-path',
|
||||||
|
_xcode.xcodeAppPath,
|
||||||
|
'--project-path',
|
||||||
|
project.xcodeProject.path,
|
||||||
|
'--workspace-path',
|
||||||
|
project.xcodeWorkspace.path,
|
||||||
|
'--device-id',
|
||||||
|
deviceId,
|
||||||
|
'--scheme',
|
||||||
|
project.scheme,
|
||||||
|
'--skip-building',
|
||||||
|
'--launch-args',
|
||||||
|
json.encode(launchArguments),
|
||||||
|
if (project.verboseLogging) '--verbose',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
final StringBuffer stdoutBuffer = StringBuffer();
|
||||||
|
stdoutSubscription = startDebugActionProcess!.stdout
|
||||||
|
.transform<String>(utf8.decoder)
|
||||||
|
.transform<String>(const LineSplitter())
|
||||||
|
.listen((String line) {
|
||||||
|
_logger.printTrace(line);
|
||||||
|
stdoutBuffer.write(line);
|
||||||
|
});
|
||||||
|
|
||||||
|
final StringBuffer stderrBuffer = StringBuffer();
|
||||||
|
bool permissionWarningPrinted = false;
|
||||||
|
// console.log from the script are found in the stderr
|
||||||
|
stderrSubscription = startDebugActionProcess!.stderr
|
||||||
|
.transform<String>(utf8.decoder)
|
||||||
|
.transform<String>(const LineSplitter())
|
||||||
|
.listen((String line) {
|
||||||
|
_logger.printTrace('stderr: $line');
|
||||||
|
stderrBuffer.write(line);
|
||||||
|
|
||||||
|
// This error may occur if Xcode automation has not been allowed.
|
||||||
|
// Example: Failed to get workspace: Error: An error occurred.
|
||||||
|
if (!permissionWarningPrinted && line.contains('Failed to get workspace') && line.contains('An error occurred')) {
|
||||||
|
_logger.printError(
|
||||||
|
'There was an error finding the project in Xcode. Ensure permission '
|
||||||
|
'has been given to control Xcode in Settings > Privacy & Security > Automation.',
|
||||||
|
);
|
||||||
|
permissionWarningPrinted = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final int exitCode = await startDebugActionProcess!.exitCode.whenComplete(() async {
|
||||||
|
await stdoutSubscription?.cancel();
|
||||||
|
await stderrSubscription?.cancel();
|
||||||
|
startDebugActionProcess = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (exitCode != 0) {
|
||||||
|
_logger.printError('Error executing osascript: $exitCode\n$stderrBuffer');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final XcodeAutomationScriptResponse? response = parseScriptResponse(
|
||||||
|
stdoutBuffer.toString(),
|
||||||
|
);
|
||||||
|
if (response == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (response.status == false) {
|
||||||
|
_logger.printError('Error starting debug session in Xcode: ${response.errorMessage}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (response.debugResult == null) {
|
||||||
|
_logger.printError('Unable to get debug results from response: $stdoutBuffer');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (response.debugResult?.status != 'running') {
|
||||||
|
_logger.printError(
|
||||||
|
'Unexpected debug results: \n'
|
||||||
|
' Status: ${response.debugResult?.status}\n'
|
||||||
|
' Completed: ${response.debugResult?.completed}\n'
|
||||||
|
' Error Message: ${response.debugResult?.errorMessage}\n'
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
} on ProcessException catch (exception) {
|
||||||
|
_logger.printError('Error executing osascript: $exitCode\n$exception');
|
||||||
|
await stdoutSubscription?.cancel();
|
||||||
|
await stderrSubscription?.cancel();
|
||||||
|
startDebugActionProcess = null;
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kills [startDebugActionProcess] if it's still running. If [force] is true, it
|
||||||
|
/// will kill all Xcode app processes. Otherwise, it will stop the debug
|
||||||
|
/// session in Xcode. If the project is temporary, it will close the Xcode
|
||||||
|
/// window of the project and then delete the project.
|
||||||
|
Future<bool> exit({
|
||||||
|
bool force = false,
|
||||||
|
@visibleForTesting
|
||||||
|
bool skipDelay = false,
|
||||||
|
}) async {
|
||||||
|
final bool success = (startDebugActionProcess == null) || startDebugActionProcess!.kill();
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
await _forceExitXcode();
|
||||||
|
if (currentDebuggingProject != null) {
|
||||||
|
final XcodeDebugProject project = currentDebuggingProject!;
|
||||||
|
if (project.isTemporaryProject) {
|
||||||
|
project.xcodeProject.parent.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
currentDebuggingProject = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentDebuggingProject != null) {
|
||||||
|
final XcodeDebugProject project = currentDebuggingProject!;
|
||||||
|
await stopDebuggingApp(
|
||||||
|
project: project,
|
||||||
|
closeXcode: project.isTemporaryProject,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (project.isTemporaryProject) {
|
||||||
|
// Wait a couple seconds before deleting the project. If project is
|
||||||
|
// still opened in Xcode and it's deleted, it will prompt the user to
|
||||||
|
// restore it.
|
||||||
|
if (!skipDelay) {
|
||||||
|
await Future<void>.delayed(const Duration(seconds: 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
project.xcodeProject.parent.deleteSync(recursive: true);
|
||||||
|
} on FileSystemException {
|
||||||
|
_logger.printError('Failed to delete temporary Xcode project: ${project.xcodeProject.parent.path}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentDebuggingProject = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kill all opened Xcode applications.
|
||||||
|
Future<bool> _forceExitXcode() async {
|
||||||
|
final RunResult result = await _processUtils.run(
|
||||||
|
<String>[
|
||||||
|
'killall',
|
||||||
|
'-9',
|
||||||
|
'Xcode',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
_logger.printError('Error killing Xcode: ${result.exitCode}\n${result.stderr}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _isProjectOpenInXcode({
|
||||||
|
required XcodeDebugProject project,
|
||||||
|
}) async {
|
||||||
|
|
||||||
|
final RunResult result = await _processUtils.run(
|
||||||
|
<String>[
|
||||||
|
..._xcode.xcrunCommand(),
|
||||||
|
'osascript',
|
||||||
|
'-l',
|
||||||
|
'JavaScript',
|
||||||
|
_xcode.xcodeAutomationScriptPath,
|
||||||
|
'check-workspace-opened',
|
||||||
|
'--xcode-path',
|
||||||
|
_xcode.xcodeAppPath,
|
||||||
|
'--project-path',
|
||||||
|
project.xcodeProject.path,
|
||||||
|
'--workspace-path',
|
||||||
|
project.xcodeWorkspace.path,
|
||||||
|
if (project.verboseLogging) '--verbose',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
_logger.printError('Error executing osascript: ${result.exitCode}\n${result.stderr}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final XcodeAutomationScriptResponse? response = parseScriptResponse(result.stdout);
|
||||||
|
if (response == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (response.status == false) {
|
||||||
|
_logger.printTrace('Error checking if project opened in Xcode: ${response.errorMessage}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
XcodeAutomationScriptResponse? parseScriptResponse(String results) {
|
||||||
|
try {
|
||||||
|
final Object decodeResult = json.decode(results) as Object;
|
||||||
|
if (decodeResult is Map<String, Object?>) {
|
||||||
|
final XcodeAutomationScriptResponse response = XcodeAutomationScriptResponse.fromJson(decodeResult);
|
||||||
|
// Status should always be found
|
||||||
|
if (response.status != null) {
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_logger.printError('osascript returned unexpected JSON response: $results');
|
||||||
|
return null;
|
||||||
|
} on FormatException {
|
||||||
|
_logger.printError('osascript returned non-JSON response: $results');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<bool> _openProjectInXcode({
|
||||||
|
required Directory xcodeWorkspace,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
await _processUtils.run(
|
||||||
|
<String>[
|
||||||
|
'open',
|
||||||
|
'-a',
|
||||||
|
_xcode.xcodeAppPath,
|
||||||
|
'-g', // Do not bring the application to the foreground.
|
||||||
|
'-j', // Launches the app hidden.
|
||||||
|
xcodeWorkspace.path
|
||||||
|
],
|
||||||
|
throwOnError: true,
|
||||||
|
);
|
||||||
|
return true;
|
||||||
|
} on ProcessException catch (error, stackTrace) {
|
||||||
|
_logger.printError('$error', stackTrace: stackTrace);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Using OSA Scripting, stop the debug session in Xcode.
|
||||||
|
///
|
||||||
|
/// If [closeXcode] is true, it will close the Xcode window that has the
|
||||||
|
/// project opened. If [promptToSaveOnClose] is true, it will ask the user if
|
||||||
|
/// they want to save any changes before it closes.
|
||||||
|
Future<bool> stopDebuggingApp({
|
||||||
|
required XcodeDebugProject project,
|
||||||
|
bool closeXcode = false,
|
||||||
|
bool promptToSaveOnClose = false,
|
||||||
|
}) async {
|
||||||
|
final RunResult result = await _processUtils.run(
|
||||||
|
<String>[
|
||||||
|
..._xcode.xcrunCommand(),
|
||||||
|
'osascript',
|
||||||
|
'-l',
|
||||||
|
'JavaScript',
|
||||||
|
_xcode.xcodeAutomationScriptPath,
|
||||||
|
'stop',
|
||||||
|
'--xcode-path',
|
||||||
|
_xcode.xcodeAppPath,
|
||||||
|
'--project-path',
|
||||||
|
project.xcodeProject.path,
|
||||||
|
'--workspace-path',
|
||||||
|
project.xcodeWorkspace.path,
|
||||||
|
if (closeXcode) '--close-window',
|
||||||
|
if (promptToSaveOnClose) '--prompt-to-save',
|
||||||
|
if (project.verboseLogging) '--verbose',
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.exitCode != 0) {
|
||||||
|
_logger.printError('Error executing osascript: ${result.exitCode}\n${result.stderr}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final XcodeAutomationScriptResponse? response = parseScriptResponse(result.stdout);
|
||||||
|
if (response == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (response.status == false) {
|
||||||
|
_logger.printError('Error stopping app in Xcode: ${response.errorMessage}');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a temporary empty Xcode project with the application bundle
|
||||||
|
/// location explicitly set.
|
||||||
|
Future<XcodeDebugProject> createXcodeProjectWithCustomBundle(
|
||||||
|
String deviceBundlePath, {
|
||||||
|
required TemplateRenderer templateRenderer,
|
||||||
|
@visibleForTesting
|
||||||
|
Directory? projectDestination,
|
||||||
|
bool verboseLogging = false,
|
||||||
|
}) async {
|
||||||
|
final Directory tempXcodeProject = projectDestination ?? _fileSystem.systemTempDirectory.createTempSync('flutter_empty_xcode.');
|
||||||
|
|
||||||
|
final Template template = await Template.fromName(
|
||||||
|
_fileSystem.path.join('xcode', 'ios', 'custom_application_bundle'),
|
||||||
|
fileSystem: _fileSystem,
|
||||||
|
templateManifest: null,
|
||||||
|
logger: _logger,
|
||||||
|
templateRenderer: templateRenderer,
|
||||||
|
);
|
||||||
|
|
||||||
|
template.render(
|
||||||
|
tempXcodeProject,
|
||||||
|
<String, Object>{
|
||||||
|
'applicationBundlePath': deviceBundlePath
|
||||||
|
},
|
||||||
|
printStatusWhenWriting: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
return XcodeDebugProject(
|
||||||
|
scheme: 'Runner',
|
||||||
|
xcodeProject: tempXcodeProject.childDirectory('Runner.xcodeproj'),
|
||||||
|
xcodeWorkspace: tempXcodeProject.childDirectory('Runner.xcworkspace'),
|
||||||
|
isTemporaryProject: true,
|
||||||
|
verboseLogging: verboseLogging,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
class XcodeAutomationScriptResponse {
|
||||||
|
XcodeAutomationScriptResponse._({
|
||||||
|
this.status,
|
||||||
|
this.errorMessage,
|
||||||
|
this.debugResult,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory XcodeAutomationScriptResponse.fromJson(Map<String, Object?> data) {
|
||||||
|
XcodeAutomationScriptDebugResult? debugResult;
|
||||||
|
if (data['debugResult'] != null && data['debugResult'] is Map<String, Object?>) {
|
||||||
|
debugResult = XcodeAutomationScriptDebugResult.fromJson(
|
||||||
|
data['debugResult']! as Map<String, Object?>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return XcodeAutomationScriptResponse._(
|
||||||
|
status: data['status'] is bool? ? data['status'] as bool? : null,
|
||||||
|
errorMessage: data['errorMessage']?.toString(),
|
||||||
|
debugResult: debugResult,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final bool? status;
|
||||||
|
final String? errorMessage;
|
||||||
|
final XcodeAutomationScriptDebugResult? debugResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
class XcodeAutomationScriptDebugResult {
|
||||||
|
XcodeAutomationScriptDebugResult._({
|
||||||
|
required this.completed,
|
||||||
|
required this.status,
|
||||||
|
required this.errorMessage,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory XcodeAutomationScriptDebugResult.fromJson(Map<String, Object?> data) {
|
||||||
|
return XcodeAutomationScriptDebugResult._(
|
||||||
|
completed: data['completed'] is bool? ? data['completed'] as bool? : null,
|
||||||
|
status: data['status']?.toString(),
|
||||||
|
errorMessage: data['errorMessage']?.toString(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether this scheme action has completed (sucessfully or otherwise). Will
|
||||||
|
/// be false if still running.
|
||||||
|
final bool? completed;
|
||||||
|
|
||||||
|
/// The status of the debug action. Potential statuses include:
|
||||||
|
/// `not yet started`, `running`, `cancelled`, `failed`, `error occurred`,
|
||||||
|
/// and `succeeded`.
|
||||||
|
///
|
||||||
|
/// Only the status of `running` indicates the debug action has started successfully.
|
||||||
|
/// For example, `succeeded` often does not indicate success as if the action fails,
|
||||||
|
/// it will sometimes return `succeeded`.
|
||||||
|
final String? status;
|
||||||
|
|
||||||
|
/// When [status] is `error occurred`, an error message is provided.
|
||||||
|
/// Otherwise, this will be null.
|
||||||
|
final String? errorMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
class XcodeDebugProject {
|
||||||
|
XcodeDebugProject({
|
||||||
|
required this.scheme,
|
||||||
|
required this.xcodeWorkspace,
|
||||||
|
required this.xcodeProject,
|
||||||
|
this.isTemporaryProject = false,
|
||||||
|
this.verboseLogging = false,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String scheme;
|
||||||
|
final Directory xcodeWorkspace;
|
||||||
|
final Directory xcodeProject;
|
||||||
|
final bool isTemporaryProject;
|
||||||
|
|
||||||
|
/// When [verboseLogging] is true, the xcode_debug.js script will log
|
||||||
|
/// additional information via console.log, which is sent to stderr.
|
||||||
|
final bool verboseLogging;
|
||||||
|
}
|
@ -8,6 +8,7 @@ import 'package:meta/meta.dart';
|
|||||||
import 'package:process/process.dart';
|
import 'package:process/process.dart';
|
||||||
|
|
||||||
import '../artifacts.dart';
|
import '../artifacts.dart';
|
||||||
|
import '../base/file_system.dart';
|
||||||
import '../base/io.dart';
|
import '../base/io.dart';
|
||||||
import '../base/logger.dart';
|
import '../base/logger.dart';
|
||||||
import '../base/platform.dart';
|
import '../base/platform.dart';
|
||||||
@ -18,10 +19,12 @@ import '../cache.dart';
|
|||||||
import '../convert.dart';
|
import '../convert.dart';
|
||||||
import '../device.dart';
|
import '../device.dart';
|
||||||
import '../globals.dart' as globals;
|
import '../globals.dart' as globals;
|
||||||
|
import '../ios/core_devices.dart';
|
||||||
import '../ios/devices.dart';
|
import '../ios/devices.dart';
|
||||||
import '../ios/ios_deploy.dart';
|
import '../ios/ios_deploy.dart';
|
||||||
import '../ios/iproxy.dart';
|
import '../ios/iproxy.dart';
|
||||||
import '../ios/mac.dart';
|
import '../ios/mac.dart';
|
||||||
|
import '../ios/xcode_debug.dart';
|
||||||
import '../reporting/reporting.dart';
|
import '../reporting/reporting.dart';
|
||||||
import 'xcode.dart';
|
import 'xcode.dart';
|
||||||
|
|
||||||
@ -65,6 +68,10 @@ class XCDevice {
|
|||||||
required Xcode xcode,
|
required Xcode xcode,
|
||||||
required Platform platform,
|
required Platform platform,
|
||||||
required IProxy iproxy,
|
required IProxy iproxy,
|
||||||
|
required FileSystem fileSystem,
|
||||||
|
@visibleForTesting
|
||||||
|
IOSCoreDeviceControl? coreDeviceControl,
|
||||||
|
XcodeDebug? xcodeDebug,
|
||||||
}) : _processUtils = ProcessUtils(logger: logger, processManager: processManager),
|
}) : _processUtils = ProcessUtils(logger: logger, processManager: processManager),
|
||||||
_logger = logger,
|
_logger = logger,
|
||||||
_iMobileDevice = IMobileDevice(
|
_iMobileDevice = IMobileDevice(
|
||||||
@ -80,6 +87,18 @@ class XCDevice {
|
|||||||
platform: platform,
|
platform: platform,
|
||||||
processManager: processManager,
|
processManager: processManager,
|
||||||
),
|
),
|
||||||
|
_coreDeviceControl = coreDeviceControl ?? IOSCoreDeviceControl(
|
||||||
|
logger: logger,
|
||||||
|
processManager: processManager,
|
||||||
|
xcode: xcode,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
),
|
||||||
|
_xcodeDebug = xcodeDebug ?? XcodeDebug(
|
||||||
|
logger: logger,
|
||||||
|
processManager: processManager,
|
||||||
|
xcode: xcode,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
),
|
||||||
_iProxy = iproxy,
|
_iProxy = iproxy,
|
||||||
_xcode = xcode {
|
_xcode = xcode {
|
||||||
|
|
||||||
@ -99,6 +118,8 @@ class XCDevice {
|
|||||||
final IOSDeploy _iosDeploy;
|
final IOSDeploy _iosDeploy;
|
||||||
final Xcode _xcode;
|
final Xcode _xcode;
|
||||||
final IProxy _iProxy;
|
final IProxy _iProxy;
|
||||||
|
final IOSCoreDeviceControl _coreDeviceControl;
|
||||||
|
final XcodeDebug _xcodeDebug;
|
||||||
|
|
||||||
List<Object>? _cachedListResults;
|
List<Object>? _cachedListResults;
|
||||||
|
|
||||||
@ -457,6 +478,17 @@ class XCDevice {
|
|||||||
return const <IOSDevice>[];
|
return const <IOSDevice>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Map<String, IOSCoreDevice> coreDeviceMap = <String, IOSCoreDevice>{};
|
||||||
|
if (_xcode.isDevicectlInstalled) {
|
||||||
|
final List<IOSCoreDevice> coreDevices = await _coreDeviceControl.getCoreDevices();
|
||||||
|
for (final IOSCoreDevice device in coreDevices) {
|
||||||
|
if (device.udid == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
coreDeviceMap[device.udid!] = device;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// [
|
// [
|
||||||
// {
|
// {
|
||||||
// "simulator" : true,
|
// "simulator" : true,
|
||||||
@ -565,11 +597,27 @@ class XCDevice {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DeviceConnectionInterface connectionInterface = _interfaceType(device);
|
||||||
|
|
||||||
|
// CoreDevices (devices with iOS 17 and greater) no longer reflect the
|
||||||
|
// correct connection interface or developer mode status in `xcdevice`.
|
||||||
|
// Use `devicectl` to get that information for CoreDevices.
|
||||||
|
final IOSCoreDevice? coreDevice = coreDeviceMap[identifier];
|
||||||
|
if (coreDevice != null) {
|
||||||
|
if (coreDevice.connectionInterface != null) {
|
||||||
|
connectionInterface = coreDevice.connectionInterface!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (coreDevice.deviceProperties?.developerModeStatus != 'enabled') {
|
||||||
|
devModeEnabled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
deviceMap[identifier] = IOSDevice(
|
deviceMap[identifier] = IOSDevice(
|
||||||
identifier,
|
identifier,
|
||||||
name: name,
|
name: name,
|
||||||
cpuArchitecture: _cpuArchitecture(device),
|
cpuArchitecture: _cpuArchitecture(device),
|
||||||
connectionInterface: _interfaceType(device),
|
connectionInterface: connectionInterface,
|
||||||
isConnected: isConnected,
|
isConnected: isConnected,
|
||||||
sdkVersion: sdkVersionString,
|
sdkVersion: sdkVersionString,
|
||||||
iProxy: _iProxy,
|
iProxy: _iProxy,
|
||||||
@ -577,8 +625,11 @@ class XCDevice {
|
|||||||
logger: _logger,
|
logger: _logger,
|
||||||
iosDeploy: _iosDeploy,
|
iosDeploy: _iosDeploy,
|
||||||
iMobileDevice: _iMobileDevice,
|
iMobileDevice: _iMobileDevice,
|
||||||
|
coreDeviceControl: _coreDeviceControl,
|
||||||
|
xcodeDebug: _xcodeDebug,
|
||||||
platform: globals.platform,
|
platform: globals.platform,
|
||||||
devModeEnabled: devModeEnabled,
|
devModeEnabled: devModeEnabled,
|
||||||
|
isCoreDevice: coreDevice != null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,8 +14,10 @@ import '../base/io.dart';
|
|||||||
import '../base/logger.dart';
|
import '../base/logger.dart';
|
||||||
import '../base/platform.dart';
|
import '../base/platform.dart';
|
||||||
import '../base/process.dart';
|
import '../base/process.dart';
|
||||||
|
import '../base/user_messages.dart';
|
||||||
import '../base/version.dart';
|
import '../base/version.dart';
|
||||||
import '../build_info.dart';
|
import '../build_info.dart';
|
||||||
|
import '../cache.dart';
|
||||||
import '../ios/xcodeproj.dart';
|
import '../ios/xcodeproj.dart';
|
||||||
|
|
||||||
Version get xcodeRequiredVersion => Version(14, null, null);
|
Version get xcodeRequiredVersion => Version(14, null, null);
|
||||||
@ -44,9 +46,13 @@ class Xcode {
|
|||||||
required Logger logger,
|
required Logger logger,
|
||||||
required FileSystem fileSystem,
|
required FileSystem fileSystem,
|
||||||
required XcodeProjectInterpreter xcodeProjectInterpreter,
|
required XcodeProjectInterpreter xcodeProjectInterpreter,
|
||||||
|
required UserMessages userMessages,
|
||||||
|
String? flutterRoot,
|
||||||
}) : _platform = platform,
|
}) : _platform = platform,
|
||||||
_fileSystem = fileSystem,
|
_fileSystem = fileSystem,
|
||||||
_xcodeProjectInterpreter = xcodeProjectInterpreter,
|
_xcodeProjectInterpreter = xcodeProjectInterpreter,
|
||||||
|
_userMessage = userMessages,
|
||||||
|
_flutterRoot = flutterRoot,
|
||||||
_processUtils =
|
_processUtils =
|
||||||
ProcessUtils(logger: logger, processManager: processManager),
|
ProcessUtils(logger: logger, processManager: processManager),
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -61,6 +67,7 @@ class Xcode {
|
|||||||
XcodeProjectInterpreter? xcodeProjectInterpreter,
|
XcodeProjectInterpreter? xcodeProjectInterpreter,
|
||||||
Platform? platform,
|
Platform? platform,
|
||||||
FileSystem? fileSystem,
|
FileSystem? fileSystem,
|
||||||
|
String? flutterRoot,
|
||||||
Logger? logger,
|
Logger? logger,
|
||||||
}) {
|
}) {
|
||||||
platform ??= FakePlatform(
|
platform ??= FakePlatform(
|
||||||
@ -72,6 +79,8 @@ class Xcode {
|
|||||||
platform: platform,
|
platform: platform,
|
||||||
processManager: processManager,
|
processManager: processManager,
|
||||||
fileSystem: fileSystem ?? MemoryFileSystem.test(),
|
fileSystem: fileSystem ?? MemoryFileSystem.test(),
|
||||||
|
userMessages: UserMessages(),
|
||||||
|
flutterRoot: flutterRoot,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
xcodeProjectInterpreter: xcodeProjectInterpreter ?? XcodeProjectInterpreter.test(processManager: processManager),
|
xcodeProjectInterpreter: xcodeProjectInterpreter ?? XcodeProjectInterpreter.test(processManager: processManager),
|
||||||
);
|
);
|
||||||
@ -81,6 +90,8 @@ class Xcode {
|
|||||||
final ProcessUtils _processUtils;
|
final ProcessUtils _processUtils;
|
||||||
final FileSystem _fileSystem;
|
final FileSystem _fileSystem;
|
||||||
final XcodeProjectInterpreter _xcodeProjectInterpreter;
|
final XcodeProjectInterpreter _xcodeProjectInterpreter;
|
||||||
|
final UserMessages _userMessage;
|
||||||
|
final String? _flutterRoot;
|
||||||
final Logger _logger;
|
final Logger _logger;
|
||||||
|
|
||||||
bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isRequiredVersionSatisfactory;
|
bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isRequiredVersionSatisfactory;
|
||||||
@ -101,6 +112,38 @@ class Xcode {
|
|||||||
return _xcodeSelectPath;
|
return _xcodeSelectPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get xcodeAppPath {
|
||||||
|
// If the Xcode Select Path is /Applications/Xcode.app/Contents/Developer,
|
||||||
|
// the path to Xcode App is /Applications/Xcode.app
|
||||||
|
|
||||||
|
final String? pathToXcode = xcodeSelectPath;
|
||||||
|
if (pathToXcode == null || pathToXcode.isEmpty) {
|
||||||
|
throwToolExit(_userMessage.xcodeMissing);
|
||||||
|
}
|
||||||
|
final int index = pathToXcode.indexOf('.app');
|
||||||
|
if (index == -1) {
|
||||||
|
throwToolExit(_userMessage.xcodeMissing);
|
||||||
|
}
|
||||||
|
return pathToXcode.substring(0, index + 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Path to script to automate debugging through Xcode. Used in xcode_debug.dart.
|
||||||
|
/// Located in this file to make it easily overrideable in google3.
|
||||||
|
String get xcodeAutomationScriptPath {
|
||||||
|
final String flutterRoot = _flutterRoot ?? Cache.flutterRoot!;
|
||||||
|
final String flutterToolsAbsolutePath = _fileSystem.path.join(
|
||||||
|
flutterRoot,
|
||||||
|
'packages',
|
||||||
|
'flutter_tools',
|
||||||
|
);
|
||||||
|
|
||||||
|
final String filePath = '$flutterToolsAbsolutePath/bin/xcode_debug.js';
|
||||||
|
if (!_fileSystem.file(filePath).existsSync()) {
|
||||||
|
throwToolExit('Unable to find Xcode automation script at $filePath');
|
||||||
|
}
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
bool get isInstalled => _xcodeProjectInterpreter.isInstalled;
|
bool get isInstalled => _xcodeProjectInterpreter.isInstalled;
|
||||||
|
|
||||||
Version? get currentVersion => _xcodeProjectInterpreter.version;
|
Version? get currentVersion => _xcodeProjectInterpreter.version;
|
||||||
@ -150,6 +193,28 @@ class Xcode {
|
|||||||
return _isSimctlInstalled ?? false;
|
return _isSimctlInstalled ?? false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool? _isDevicectlInstalled;
|
||||||
|
|
||||||
|
/// Verifies that `devicectl` is installed by checking Xcode version and trying
|
||||||
|
/// to run it. `devicectl` is made available in Xcode 15.
|
||||||
|
bool get isDevicectlInstalled {
|
||||||
|
if (_isDevicectlInstalled == null) {
|
||||||
|
try {
|
||||||
|
if (currentVersion == null || currentVersion!.major < 15) {
|
||||||
|
_isDevicectlInstalled = false;
|
||||||
|
return _isDevicectlInstalled!;
|
||||||
|
}
|
||||||
|
final RunResult result = _processUtils.runSync(
|
||||||
|
<String>[...xcrunCommand(), 'devicectl', '--version'],
|
||||||
|
);
|
||||||
|
_isDevicectlInstalled = result.exitCode == 0;
|
||||||
|
} on ProcessException {
|
||||||
|
_isDevicectlInstalled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _isDevicectlInstalled ?? false;
|
||||||
|
}
|
||||||
|
|
||||||
bool get isRequiredVersionSatisfactory {
|
bool get isRequiredVersionSatisfactory {
|
||||||
final Version? version = currentVersion;
|
final Version? version = currentVersion;
|
||||||
if (version == null) {
|
if (version == null) {
|
||||||
|
@ -130,9 +130,9 @@ class MDnsVmServiceDiscovery {
|
|||||||
/// The [deviceVmservicePort] parameter must be set to specify which port
|
/// The [deviceVmservicePort] parameter must be set to specify which port
|
||||||
/// to find.
|
/// to find.
|
||||||
///
|
///
|
||||||
/// [applicationId] and [deviceVmservicePort] are required for launch so that
|
/// [applicationId] and either [deviceVmservicePort] or [deviceName] are
|
||||||
/// if multiple flutter apps are running on different devices, it will
|
/// required for launch so that if multiple flutter apps are running on
|
||||||
/// only match with the device running the desired app.
|
/// different devices, it will only match with the device running the desired app.
|
||||||
///
|
///
|
||||||
/// The [useDeviceIPAsHost] parameter flags whether to get the device IP
|
/// The [useDeviceIPAsHost] parameter flags whether to get the device IP
|
||||||
/// and the [ipv6] parameter flags whether to get an iPv6 address
|
/// and the [ipv6] parameter flags whether to get an iPv6 address
|
||||||
@ -141,21 +141,27 @@ class MDnsVmServiceDiscovery {
|
|||||||
/// The [timeout] parameter determines how long to continue to wait for
|
/// The [timeout] parameter determines how long to continue to wait for
|
||||||
/// services to become active.
|
/// services to become active.
|
||||||
///
|
///
|
||||||
/// If a Dart VM Service matching the [applicationId] and [deviceVmservicePort]
|
/// If a Dart VM Service matching the [applicationId] and
|
||||||
/// cannot be found after the [timeout], it will call [throwToolExit].
|
/// [deviceVmservicePort]/[deviceName] cannot be found before the [timeout]
|
||||||
|
/// is reached, it will call [throwToolExit].
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
Future<MDnsVmServiceDiscoveryResult?> queryForLaunch({
|
Future<MDnsVmServiceDiscoveryResult?> queryForLaunch({
|
||||||
required String applicationId,
|
required String applicationId,
|
||||||
required int deviceVmservicePort,
|
int? deviceVmservicePort,
|
||||||
|
String? deviceName,
|
||||||
bool ipv6 = false,
|
bool ipv6 = false,
|
||||||
bool useDeviceIPAsHost = false,
|
bool useDeviceIPAsHost = false,
|
||||||
Duration timeout = const Duration(minutes: 10),
|
Duration timeout = const Duration(minutes: 10),
|
||||||
}) async {
|
}) async {
|
||||||
// Query for a specific application and device port.
|
// Either the device port or the device name must be provided.
|
||||||
|
assert(deviceVmservicePort != null || deviceName != null);
|
||||||
|
|
||||||
|
// Query for a specific application matching on either device port or device name.
|
||||||
return firstMatchingVmService(
|
return firstMatchingVmService(
|
||||||
_client,
|
_client,
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
deviceVmservicePort: deviceVmservicePort,
|
deviceVmservicePort: deviceVmservicePort,
|
||||||
|
deviceName: deviceName,
|
||||||
ipv6: ipv6,
|
ipv6: ipv6,
|
||||||
useDeviceIPAsHost: useDeviceIPAsHost,
|
useDeviceIPAsHost: useDeviceIPAsHost,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
@ -170,6 +176,7 @@ class MDnsVmServiceDiscovery {
|
|||||||
MDnsClient client, {
|
MDnsClient client, {
|
||||||
String? applicationId,
|
String? applicationId,
|
||||||
int? deviceVmservicePort,
|
int? deviceVmservicePort,
|
||||||
|
String? deviceName,
|
||||||
bool ipv6 = false,
|
bool ipv6 = false,
|
||||||
bool useDeviceIPAsHost = false,
|
bool useDeviceIPAsHost = false,
|
||||||
Duration timeout = const Duration(minutes: 10),
|
Duration timeout = const Duration(minutes: 10),
|
||||||
@ -178,6 +185,7 @@ class MDnsVmServiceDiscovery {
|
|||||||
client,
|
client,
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
deviceVmservicePort: deviceVmservicePort,
|
deviceVmservicePort: deviceVmservicePort,
|
||||||
|
deviceName: deviceName,
|
||||||
ipv6: ipv6,
|
ipv6: ipv6,
|
||||||
useDeviceIPAsHost: useDeviceIPAsHost,
|
useDeviceIPAsHost: useDeviceIPAsHost,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
@ -193,6 +201,7 @@ class MDnsVmServiceDiscovery {
|
|||||||
MDnsClient client, {
|
MDnsClient client, {
|
||||||
String? applicationId,
|
String? applicationId,
|
||||||
int? deviceVmservicePort,
|
int? deviceVmservicePort,
|
||||||
|
String? deviceName,
|
||||||
bool ipv6 = false,
|
bool ipv6 = false,
|
||||||
bool useDeviceIPAsHost = false,
|
bool useDeviceIPAsHost = false,
|
||||||
required Duration timeout,
|
required Duration timeout,
|
||||||
@ -263,6 +272,11 @@ class MDnsVmServiceDiscovery {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If deviceName is set, only use records that match it
|
||||||
|
if (deviceName != null && !deviceNameMatchesTargetName(deviceName, srvRecord.target)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Get the IP address of the device if using the IP as the host.
|
// Get the IP address of the device if using the IP as the host.
|
||||||
InternetAddress? ipAddress;
|
InternetAddress? ipAddress;
|
||||||
if (useDeviceIPAsHost) {
|
if (useDeviceIPAsHost) {
|
||||||
@ -332,6 +346,15 @@ class MDnsVmServiceDiscovery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@visibleForTesting
|
||||||
|
bool deviceNameMatchesTargetName(String deviceName, String targetName) {
|
||||||
|
// Remove `.local` from the name along with any non-word, non-digit characters.
|
||||||
|
final RegExp cleanedNameRegex = RegExp(r'\.local|\W');
|
||||||
|
final String cleanedDeviceName = deviceName.trim().toLowerCase().replaceAll(cleanedNameRegex, '');
|
||||||
|
final String cleanedTargetName = targetName.toLowerCase().replaceAll(cleanedNameRegex, '');
|
||||||
|
return cleanedDeviceName == cleanedTargetName;
|
||||||
|
}
|
||||||
|
|
||||||
String _getAuthCode(String txtRecord) {
|
String _getAuthCode(String txtRecord) {
|
||||||
const String authCodePrefix = 'authCode=';
|
const String authCodePrefix = 'authCode=';
|
||||||
final Iterable<String> matchingRecords =
|
final Iterable<String> matchingRecords =
|
||||||
@ -354,7 +377,7 @@ class MDnsVmServiceDiscovery {
|
|||||||
/// When [useDeviceIPAsHost] is true, it will use the device's IP as the
|
/// When [useDeviceIPAsHost] is true, it will use the device's IP as the
|
||||||
/// host and will not forward the port.
|
/// host and will not forward the port.
|
||||||
///
|
///
|
||||||
/// Differs from `getVMServiceUriForLaunch` because it can search for any available Dart VM Service.
|
/// Differs from [getVMServiceUriForLaunch] because it can search for any available Dart VM Service.
|
||||||
/// Since [applicationId] and [deviceVmservicePort] are optional, it can either look for any service
|
/// Since [applicationId] and [deviceVmservicePort] are optional, it can either look for any service
|
||||||
/// or a specific service matching [applicationId]/[deviceVmservicePort].
|
/// or a specific service matching [applicationId]/[deviceVmservicePort].
|
||||||
/// It may find more than one service, which will throw an error listing the found services.
|
/// It may find more than one service, which will throw an error listing the found services.
|
||||||
@ -391,20 +414,22 @@ class MDnsVmServiceDiscovery {
|
|||||||
/// When [useDeviceIPAsHost] is true, it will use the device's IP as the
|
/// When [useDeviceIPAsHost] is true, it will use the device's IP as the
|
||||||
/// host and will not forward the port.
|
/// host and will not forward the port.
|
||||||
///
|
///
|
||||||
/// Differs from `getVMServiceUriForAttach` because it only searches for a specific service.
|
/// Differs from [getVMServiceUriForAttach] because it only searches for a specific service.
|
||||||
/// This is enforced by [applicationId] and [deviceVmservicePort] being required.
|
/// This is enforced by [applicationId] being required and using either the
|
||||||
|
/// [deviceVmservicePort] or the [device]'s name to query.
|
||||||
Future<Uri?> getVMServiceUriForLaunch(
|
Future<Uri?> getVMServiceUriForLaunch(
|
||||||
String applicationId,
|
String applicationId,
|
||||||
Device device, {
|
Device device, {
|
||||||
bool usesIpv6 = false,
|
bool usesIpv6 = false,
|
||||||
int? hostVmservicePort,
|
int? hostVmservicePort,
|
||||||
required int deviceVmservicePort,
|
int? deviceVmservicePort,
|
||||||
bool useDeviceIPAsHost = false,
|
bool useDeviceIPAsHost = false,
|
||||||
Duration timeout = const Duration(minutes: 10),
|
Duration timeout = const Duration(minutes: 10),
|
||||||
}) async {
|
}) async {
|
||||||
final MDnsVmServiceDiscoveryResult? result = await queryForLaunch(
|
final MDnsVmServiceDiscoveryResult? result = await queryForLaunch(
|
||||||
applicationId: applicationId,
|
applicationId: applicationId,
|
||||||
deviceVmservicePort: deviceVmservicePort,
|
deviceVmservicePort: deviceVmservicePort,
|
||||||
|
deviceName: deviceVmservicePort == null ? device.name : null,
|
||||||
ipv6: usesIpv6,
|
ipv6: usesIpv6,
|
||||||
useDeviceIPAsHost: useDeviceIPAsHost,
|
useDeviceIPAsHost: useDeviceIPAsHost,
|
||||||
timeout: timeout,
|
timeout: timeout,
|
||||||
|
@ -339,6 +339,15 @@
|
|||||||
"templates/skeleton/README.md.tmpl",
|
"templates/skeleton/README.md.tmpl",
|
||||||
"templates/skeleton/test/implementation_test.dart.test.tmpl",
|
"templates/skeleton/test/implementation_test.dart.test.tmpl",
|
||||||
"templates/skeleton/test/unit_test.dart.tmpl",
|
"templates/skeleton/test/unit_test.dart.tmpl",
|
||||||
"templates/skeleton/test/widget_test.dart.tmpl"
|
"templates/skeleton/test/widget_test.dart.tmpl",
|
||||||
|
|
||||||
|
"templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/contents.xcworkspacedata",
|
||||||
|
"templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/IDEWorkspaceChecks.plist",
|
||||||
|
"templates/xcode/ios/custom_application_bundle/Runner.xcworkspace.tmpl/xcshareddata/WorkspaceSettings.xcsettings",
|
||||||
|
"templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.pbxproj",
|
||||||
|
"templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/contents.xcworkspacedata",
|
||||||
|
"templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist",
|
||||||
|
"templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings",
|
||||||
|
"templates/xcode/ios/custom_application_bundle/Runner.xcodeproj.tmpl/xcshareddata/xcschemes/Runner.xcscheme.tmpl"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
# Template Xcode project with a custom application bundle
|
||||||
|
|
||||||
|
This template is an empty Xcode project with a settable application bundle path
|
||||||
|
within the `xcscheme`. It is used when debugging a project on a physical iOS 17+
|
||||||
|
device via Xcode 15+ when `--use-application-binary` is used.
|
@ -0,0 +1,297 @@
|
|||||||
|
// !$*UTF8*$!
|
||||||
|
{
|
||||||
|
archiveVersion = 1;
|
||||||
|
classes = {
|
||||||
|
};
|
||||||
|
objectVersion = 54;
|
||||||
|
objects = {
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastUpgradeCheck = 1430;
|
||||||
|
ORGANIZATIONNAME = "";
|
||||||
|
TargetAttributes = {
|
||||||
|
97C146ED1CF9000F007C117D = {
|
||||||
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
|
LastSwiftMigration = 1100;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||||
|
compatibilityVersion = "Xcode 9.3";
|
||||||
|
developmentRegion = en;
|
||||||
|
hasScannedForEncodings = 0;
|
||||||
|
knownRegions = (
|
||||||
|
en,
|
||||||
|
Base,
|
||||||
|
);
|
||||||
|
mainGroup = 97C146E51CF9000F007C117D;
|
||||||
|
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||||
|
projectDirPath = "";
|
||||||
|
projectRoot = "";
|
||||||
|
targets = (
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
/* End PBXProject section */
|
||||||
|
|
||||||
|
/* Begin PBXResourcesBuildPhase section */
|
||||||
|
331C807F294A63A400263BE5 /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Profile;
|
||||||
|
};
|
||||||
|
97C147031CF9000F007C117D /* Debug */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
ENABLE_TESTABILITY = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_DYNAMIC_NO_PIC = NO;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_OPTIMIZATION_LEVEL = 0;
|
||||||
|
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||||
|
"DEBUG=1",
|
||||||
|
"$(inherited)",
|
||||||
|
);
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = YES;
|
||||||
|
ONLY_ACTIVE_ARCH = YES;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
};
|
||||||
|
name = Debug;
|
||||||
|
};
|
||||||
|
97C147041CF9000F007C117D /* Release */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
CLANG_ANALYZER_NONNULL = YES;
|
||||||
|
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
|
||||||
|
CLANG_CXX_LIBRARY = "libc++";
|
||||||
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CLANG_ENABLE_OBJC_ARC = YES;
|
||||||
|
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
|
||||||
|
CLANG_WARN_BOOL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_COMMA = YES;
|
||||||
|
CLANG_WARN_CONSTANT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
|
||||||
|
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
|
||||||
|
CLANG_WARN_EMPTY_BODY = YES;
|
||||||
|
CLANG_WARN_ENUM_CONVERSION = YES;
|
||||||
|
CLANG_WARN_INFINITE_RECURSION = YES;
|
||||||
|
CLANG_WARN_INT_CONVERSION = YES;
|
||||||
|
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
|
||||||
|
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
|
||||||
|
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
|
||||||
|
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
|
||||||
|
CLANG_WARN_STRICT_PROTOTYPES = YES;
|
||||||
|
CLANG_WARN_SUSPICIOUS_MOVE = YES;
|
||||||
|
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||||
|
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||||
|
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
|
||||||
|
COPY_PHASE_STRIP = NO;
|
||||||
|
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||||
|
ENABLE_NS_ASSERTIONS = NO;
|
||||||
|
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||||
|
GCC_C_LANGUAGE_STANDARD = gnu99;
|
||||||
|
GCC_NO_COMMON_BLOCKS = YES;
|
||||||
|
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
|
||||||
|
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||||
|
GCC_WARN_UNDECLARED_SELECTOR = YES;
|
||||||
|
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||||
|
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||||
|
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||||
|
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
|
||||||
|
MTL_ENABLE_DEBUG_INFO = NO;
|
||||||
|
SDKROOT = iphoneos;
|
||||||
|
SUPPORTED_PLATFORMS = iphoneos;
|
||||||
|
SWIFT_COMPILATION_MODE = wholemodule;
|
||||||
|
SWIFT_OPTIMIZATION_LEVEL = "-O";
|
||||||
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
|
VALIDATE_PRODUCT = YES;
|
||||||
|
};
|
||||||
|
name = Release;
|
||||||
|
};
|
||||||
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
|
/* Begin XCConfigurationList section */
|
||||||
|
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147031CF9000F007C117D /* Debug */,
|
||||||
|
97C147041CF9000F007C117D /* Release */,
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
|
||||||
|
isa = XCConfigurationList;
|
||||||
|
buildConfigurations = (
|
||||||
|
97C147061CF9000F007C117D /* Debug */,
|
||||||
|
97C147071CF9000F007C117D /* Release */,
|
||||||
|
249021D4217E4FDB00AE95B9 /* Profile */,
|
||||||
|
);
|
||||||
|
defaultConfigurationIsVisible = 0;
|
||||||
|
defaultConfigurationName = Release;
|
||||||
|
};
|
||||||
|
/* End XCConfigurationList section */
|
||||||
|
};
|
||||||
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
}
|
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -0,0 +1,82 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1430"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<PathRunnable
|
||||||
|
runnableDebuggingMode = "0"
|
||||||
|
FilePath = "{{applicationBundlePath}}">
|
||||||
|
</PathRunnable>
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<PathRunnable
|
||||||
|
runnableDebuggingMode = "0"
|
||||||
|
FilePath = "{{applicationBundlePath}}">
|
||||||
|
</PathRunnable>
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
@ -884,6 +884,26 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWithoutContext('Get launch arguments for physical CoreDevice with debugging enabled with no launch arguments', () {
|
||||||
|
final DebuggingOptions original = DebuggingOptions.enabled(
|
||||||
|
BuildInfo.debug,
|
||||||
|
);
|
||||||
|
|
||||||
|
final List<String> launchArguments = original.getIOSLaunchArguments(
|
||||||
|
EnvironmentType.physical,
|
||||||
|
null,
|
||||||
|
<String, Object?>{},
|
||||||
|
isCoreDevice: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
launchArguments.join(' '),
|
||||||
|
<String>[
|
||||||
|
'--enable-dart-profiling',
|
||||||
|
].join(' '),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
testWithoutContext('Get launch arguments for physical device with iPv4 network connection', () {
|
testWithoutContext('Get launch arguments for physical device with iPv4 network connection', () {
|
||||||
final DebuggingOptions original = DebuggingOptions.enabled(
|
final DebuggingOptions original = DebuggingOptions.enabled(
|
||||||
BuildInfo.debug,
|
BuildInfo.debug,
|
||||||
|
1949
packages/flutter_tools/test/general.shard/ios/core_devices_test.dart
Normal file
1949
packages/flutter_tools/test/general.shard/ios/core_devices_test.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -19,11 +19,13 @@ import 'package:flutter_tools/src/cache.dart';
|
|||||||
import 'package:flutter_tools/src/device.dart';
|
import 'package:flutter_tools/src/device.dart';
|
||||||
import 'package:flutter_tools/src/device_port_forwarder.dart';
|
import 'package:flutter_tools/src/device_port_forwarder.dart';
|
||||||
import 'package:flutter_tools/src/ios/application_package.dart';
|
import 'package:flutter_tools/src/ios/application_package.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/core_devices.dart';
|
||||||
import 'package:flutter_tools/src/ios/devices.dart';
|
import 'package:flutter_tools/src/ios/devices.dart';
|
||||||
import 'package:flutter_tools/src/ios/ios_deploy.dart';
|
import 'package:flutter_tools/src/ios/ios_deploy.dart';
|
||||||
import 'package:flutter_tools/src/ios/ios_workflow.dart';
|
import 'package:flutter_tools/src/ios/ios_workflow.dart';
|
||||||
import 'package:flutter_tools/src/ios/iproxy.dart';
|
import 'package:flutter_tools/src/ios/iproxy.dart';
|
||||||
import 'package:flutter_tools/src/ios/mac.dart';
|
import 'package:flutter_tools/src/ios/mac.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/xcode_debug.dart';
|
||||||
import 'package:flutter_tools/src/macos/xcdevice.dart';
|
import 'package:flutter_tools/src/macos/xcdevice.dart';
|
||||||
import 'package:test/fake.dart';
|
import 'package:test/fake.dart';
|
||||||
|
|
||||||
@ -42,6 +44,8 @@ void main() {
|
|||||||
late IOSDeploy iosDeploy;
|
late IOSDeploy iosDeploy;
|
||||||
late IMobileDevice iMobileDevice;
|
late IMobileDevice iMobileDevice;
|
||||||
late FileSystem fileSystem;
|
late FileSystem fileSystem;
|
||||||
|
late IOSCoreDeviceControl coreDeviceControl;
|
||||||
|
late XcodeDebug xcodeDebug;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
final Artifacts artifacts = Artifacts.test();
|
final Artifacts artifacts = Artifacts.test();
|
||||||
@ -61,6 +65,8 @@ void main() {
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
processManager: FakeProcessManager.any(),
|
processManager: FakeProcessManager.any(),
|
||||||
);
|
);
|
||||||
|
coreDeviceControl = FakeIOSCoreDeviceControl();
|
||||||
|
xcodeDebug = FakeXcodeDebug();
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('successfully instantiates on Mac OS', () {
|
testWithoutContext('successfully instantiates on Mac OS', () {
|
||||||
@ -72,12 +78,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
sdkVersion: '13.3',
|
sdkVersion: '13.3',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
);
|
);
|
||||||
expect(device.isSupported(), isTrue);
|
expect(device.isSupported(), isTrue);
|
||||||
});
|
});
|
||||||
@ -91,11 +100,14 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
cpuArchitecture: DarwinArch.armv7,
|
cpuArchitecture: DarwinArch.armv7,
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
);
|
);
|
||||||
expect(device.isSupported(), isFalse);
|
expect(device.isSupported(), isFalse);
|
||||||
});
|
});
|
||||||
@ -109,12 +121,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
sdkVersion: '1.0.0',
|
sdkVersion: '1.0.0',
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
).majorSdkVersion, 1);
|
).majorSdkVersion, 1);
|
||||||
expect(IOSDevice(
|
expect(IOSDevice(
|
||||||
'device-123',
|
'device-123',
|
||||||
@ -124,12 +139,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
sdkVersion: '13.1.1',
|
sdkVersion: '13.1.1',
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
).majorSdkVersion, 13);
|
).majorSdkVersion, 13);
|
||||||
expect(IOSDevice(
|
expect(IOSDevice(
|
||||||
'device-123',
|
'device-123',
|
||||||
@ -139,12 +157,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
sdkVersion: '10',
|
sdkVersion: '10',
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
).majorSdkVersion, 10);
|
).majorSdkVersion, 10);
|
||||||
expect(IOSDevice(
|
expect(IOSDevice(
|
||||||
'device-123',
|
'device-123',
|
||||||
@ -154,12 +175,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
sdkVersion: '0',
|
sdkVersion: '0',
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
).majorSdkVersion, 0);
|
).majorSdkVersion, 0);
|
||||||
expect(IOSDevice(
|
expect(IOSDevice(
|
||||||
'device-123',
|
'device-123',
|
||||||
@ -169,12 +193,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
sdkVersion: 'bogus',
|
sdkVersion: 'bogus',
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
).majorSdkVersion, 0);
|
).majorSdkVersion, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -187,12 +214,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
sdkVersion: '13.3.1',
|
sdkVersion: '13.3.1',
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
).sdkVersion;
|
).sdkVersion;
|
||||||
Version expectedVersion = Version(13, 3, 1, text: '13.3.1');
|
Version expectedVersion = Version(13, 3, 1, text: '13.3.1');
|
||||||
expect(sdkVersion, isNotNull);
|
expect(sdkVersion, isNotNull);
|
||||||
@ -207,12 +237,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
sdkVersion: '13.3.1 (20ADBC)',
|
sdkVersion: '13.3.1 (20ADBC)',
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
).sdkVersion;
|
).sdkVersion;
|
||||||
expectedVersion = Version(13, 3, 1, text: '13.3.1 (20ADBC)');
|
expectedVersion = Version(13, 3, 1, text: '13.3.1 (20ADBC)');
|
||||||
expect(sdkVersion, isNotNull);
|
expect(sdkVersion, isNotNull);
|
||||||
@ -227,12 +260,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
sdkVersion: '16.4.1(a) (20ADBC)',
|
sdkVersion: '16.4.1(a) (20ADBC)',
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
).sdkVersion;
|
).sdkVersion;
|
||||||
expectedVersion = Version(16, 4, 1, text: '16.4.1(a) (20ADBC)');
|
expectedVersion = Version(16, 4, 1, text: '16.4.1(a) (20ADBC)');
|
||||||
expect(sdkVersion, isNotNull);
|
expect(sdkVersion, isNotNull);
|
||||||
@ -247,12 +283,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
sdkVersion: '0',
|
sdkVersion: '0',
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
).sdkVersion;
|
).sdkVersion;
|
||||||
expectedVersion = Version(0, 0, 0, text: '0');
|
expectedVersion = Version(0, 0, 0, text: '0');
|
||||||
expect(sdkVersion, isNotNull);
|
expect(sdkVersion, isNotNull);
|
||||||
@ -267,11 +306,14 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
).sdkVersion;
|
).sdkVersion;
|
||||||
expect(sdkVersion, isNull);
|
expect(sdkVersion, isNull);
|
||||||
|
|
||||||
@ -283,12 +325,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
sdkVersion: 'bogus',
|
sdkVersion: 'bogus',
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
).sdkVersion;
|
).sdkVersion;
|
||||||
expect(sdkVersion, isNull);
|
expect(sdkVersion, isNull);
|
||||||
});
|
});
|
||||||
@ -302,12 +347,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
sdkVersion: '13.3 17C54',
|
sdkVersion: '13.3 17C54',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(await device.sdkNameAndVersion,'iOS 13.3 17C54');
|
expect(await device.sdkNameAndVersion,'iOS 13.3 17C54');
|
||||||
@ -322,12 +370,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
sdkVersion: '13.3',
|
sdkVersion: '13.3',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(device.supportsRuntimeMode(BuildMode.debug), true);
|
expect(device.supportsRuntimeMode(BuildMode.debug), true);
|
||||||
@ -348,12 +399,15 @@ void main() {
|
|||||||
platform: platform,
|
platform: platform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
sdkVersion: '13.3',
|
sdkVersion: '13.3',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
throwsAssertionError,
|
throwsAssertionError,
|
||||||
@ -440,12 +494,15 @@ void main() {
|
|||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
sdkVersion: '13.3',
|
sdkVersion: '13.3',
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
);
|
);
|
||||||
logReader1 = createLogReader(device, appPackage1, process1);
|
logReader1 = createLogReader(device, appPackage1, process1);
|
||||||
logReader2 = createLogReader(device, appPackage2, process2);
|
logReader2 = createLogReader(device, appPackage2, process2);
|
||||||
@ -471,6 +528,8 @@ void main() {
|
|||||||
late IOSDeploy iosDeploy;
|
late IOSDeploy iosDeploy;
|
||||||
late IMobileDevice iMobileDevice;
|
late IMobileDevice iMobileDevice;
|
||||||
late IOSWorkflow iosWorkflow;
|
late IOSWorkflow iosWorkflow;
|
||||||
|
late IOSCoreDeviceControl coreDeviceControl;
|
||||||
|
late XcodeDebug xcodeDebug;
|
||||||
late IOSDevice device1;
|
late IOSDevice device1;
|
||||||
late IOSDevice device2;
|
late IOSDevice device2;
|
||||||
|
|
||||||
@ -494,6 +553,8 @@ void main() {
|
|||||||
processManager: fakeProcessManager,
|
processManager: fakeProcessManager,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
);
|
);
|
||||||
|
coreDeviceControl = FakeIOSCoreDeviceControl();
|
||||||
|
xcodeDebug = FakeXcodeDebug();
|
||||||
|
|
||||||
device1 = IOSDevice(
|
device1 = IOSDevice(
|
||||||
'd83d5bc53967baa0ee18626ba87b6254b2ab5418',
|
'd83d5bc53967baa0ee18626ba87b6254b2ab5418',
|
||||||
@ -503,12 +564,15 @@ void main() {
|
|||||||
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
|
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
fileSystem: MemoryFileSystem.test(),
|
fileSystem: MemoryFileSystem.test(),
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
device2 = IOSDevice(
|
device2 = IOSDevice(
|
||||||
@ -519,12 +583,15 @@ void main() {
|
|||||||
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
|
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
fileSystem: MemoryFileSystem.test(),
|
fileSystem: MemoryFileSystem.test(),
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -781,6 +848,8 @@ void main() {
|
|||||||
late IOSDeploy iosDeploy;
|
late IOSDeploy iosDeploy;
|
||||||
late IMobileDevice iMobileDevice;
|
late IMobileDevice iMobileDevice;
|
||||||
late IOSWorkflow iosWorkflow;
|
late IOSWorkflow iosWorkflow;
|
||||||
|
late IOSCoreDeviceControl coreDeviceControl;
|
||||||
|
late XcodeDebug xcodeDebug;
|
||||||
late IOSDevice notConnected1;
|
late IOSDevice notConnected1;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
@ -803,6 +872,8 @@ void main() {
|
|||||||
processManager: fakeProcessManager,
|
processManager: fakeProcessManager,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
);
|
);
|
||||||
|
coreDeviceControl = FakeIOSCoreDeviceControl();
|
||||||
|
xcodeDebug = FakeXcodeDebug();
|
||||||
notConnected1 = IOSDevice(
|
notConnected1 = IOSDevice(
|
||||||
'00000001-0000000000000000',
|
'00000001-0000000000000000',
|
||||||
name: 'iPad',
|
name: 'iPad',
|
||||||
@ -811,12 +882,15 @@ void main() {
|
|||||||
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
|
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
|
||||||
iosDeploy: iosDeploy,
|
iosDeploy: iosDeploy,
|
||||||
iMobileDevice: iMobileDevice,
|
iMobileDevice: iMobileDevice,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
platform: macPlatform,
|
platform: macPlatform,
|
||||||
fileSystem: MemoryFileSystem.test(),
|
fileSystem: MemoryFileSystem.test(),
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: false,
|
isConnected: false,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -965,3 +1039,10 @@ class FakeProcess extends Fake implements Process {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FakeXcodeDebug extends Fake implements XcodeDebug {
|
||||||
|
@override
|
||||||
|
bool get debugStarted => false;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {}
|
||||||
|
@ -12,10 +12,13 @@ import 'package:flutter_tools/src/build_info.dart';
|
|||||||
import 'package:flutter_tools/src/cache.dart';
|
import 'package:flutter_tools/src/cache.dart';
|
||||||
import 'package:flutter_tools/src/device.dart';
|
import 'package:flutter_tools/src/device.dart';
|
||||||
import 'package:flutter_tools/src/ios/application_package.dart';
|
import 'package:flutter_tools/src/ios/application_package.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/core_devices.dart';
|
||||||
import 'package:flutter_tools/src/ios/devices.dart';
|
import 'package:flutter_tools/src/ios/devices.dart';
|
||||||
import 'package:flutter_tools/src/ios/ios_deploy.dart';
|
import 'package:flutter_tools/src/ios/ios_deploy.dart';
|
||||||
import 'package:flutter_tools/src/ios/iproxy.dart';
|
import 'package:flutter_tools/src/ios/iproxy.dart';
|
||||||
import 'package:flutter_tools/src/ios/mac.dart';
|
import 'package:flutter_tools/src/ios/mac.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/xcode_debug.dart';
|
||||||
|
import 'package:test/fake.dart';
|
||||||
|
|
||||||
import '../../src/common.dart';
|
import '../../src/common.dart';
|
||||||
import '../../src/fake_process_manager.dart';
|
import '../../src/fake_process_manager.dart';
|
||||||
@ -105,6 +108,28 @@ void main() {
|
|||||||
expect(processManager, hasNoRemainingExpectations);
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWithoutContext('IOSDevice.installApp uses devicectl for CoreDevices', () async {
|
||||||
|
final IOSApp iosApp = PrebuiltIOSApp(
|
||||||
|
projectBundleId: 'app',
|
||||||
|
uncompressedBundle: fileSystem.currentDirectory,
|
||||||
|
applicationPackage: bundleDirectory,
|
||||||
|
);
|
||||||
|
|
||||||
|
final FakeProcessManager processManager = FakeProcessManager.empty();
|
||||||
|
|
||||||
|
final IOSDevice device = setUpIOSDevice(
|
||||||
|
processManager: processManager,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
interfaceType: DeviceConnectionInterface.attached,
|
||||||
|
artifacts: artifacts,
|
||||||
|
isCoreDevice: true,
|
||||||
|
);
|
||||||
|
final bool wasInstalled = await device.installApp(iosApp);
|
||||||
|
|
||||||
|
expect(wasInstalled, true);
|
||||||
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
|
});
|
||||||
|
|
||||||
testWithoutContext('IOSDevice.uninstallApp calls ios-deploy correctly', () async {
|
testWithoutContext('IOSDevice.uninstallApp calls ios-deploy correctly', () async {
|
||||||
final IOSApp iosApp = PrebuiltIOSApp(
|
final IOSApp iosApp = PrebuiltIOSApp(
|
||||||
projectBundleId: 'app',
|
projectBundleId: 'app',
|
||||||
@ -134,6 +159,28 @@ void main() {
|
|||||||
expect(processManager, hasNoRemainingExpectations);
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWithoutContext('IOSDevice.uninstallApp uses devicectl for CoreDevices', () async {
|
||||||
|
final IOSApp iosApp = PrebuiltIOSApp(
|
||||||
|
projectBundleId: 'app',
|
||||||
|
uncompressedBundle: fileSystem.currentDirectory,
|
||||||
|
applicationPackage: bundleDirectory,
|
||||||
|
);
|
||||||
|
|
||||||
|
final FakeProcessManager processManager = FakeProcessManager.empty();
|
||||||
|
|
||||||
|
final IOSDevice device = setUpIOSDevice(
|
||||||
|
processManager: processManager,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
interfaceType: DeviceConnectionInterface.attached,
|
||||||
|
artifacts: artifacts,
|
||||||
|
isCoreDevice: true,
|
||||||
|
);
|
||||||
|
final bool wasUninstalled = await device.uninstallApp(iosApp);
|
||||||
|
|
||||||
|
expect(wasUninstalled, true);
|
||||||
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
|
});
|
||||||
|
|
||||||
group('isAppInstalled', () {
|
group('isAppInstalled', () {
|
||||||
testWithoutContext('catches ProcessException from ios-deploy', () async {
|
testWithoutContext('catches ProcessException from ios-deploy', () async {
|
||||||
final IOSApp iosApp = PrebuiltIOSApp(
|
final IOSApp iosApp = PrebuiltIOSApp(
|
||||||
@ -263,6 +310,28 @@ void main() {
|
|||||||
expect(processManager, hasNoRemainingExpectations);
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
expect(logger.traceText, contains(stderr));
|
expect(logger.traceText, contains(stderr));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWithoutContext('uses devicectl for CoreDevices', () async {
|
||||||
|
final IOSApp iosApp = PrebuiltIOSApp(
|
||||||
|
projectBundleId: 'app',
|
||||||
|
uncompressedBundle: fileSystem.currentDirectory,
|
||||||
|
applicationPackage: bundleDirectory,
|
||||||
|
);
|
||||||
|
|
||||||
|
final FakeProcessManager processManager = FakeProcessManager.empty();
|
||||||
|
|
||||||
|
final IOSDevice device = setUpIOSDevice(
|
||||||
|
processManager: processManager,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
interfaceType: DeviceConnectionInterface.attached,
|
||||||
|
artifacts: artifacts,
|
||||||
|
isCoreDevice: true,
|
||||||
|
);
|
||||||
|
final bool wasInstalled = await device.isAppInstalled(iosApp);
|
||||||
|
|
||||||
|
expect(wasInstalled, true);
|
||||||
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('IOSDevice.installApp catches ProcessException from ios-deploy', () async {
|
testWithoutContext('IOSDevice.installApp catches ProcessException from ios-deploy', () async {
|
||||||
@ -314,6 +383,8 @@ void main() {
|
|||||||
|
|
||||||
expect(wasAppUninstalled, false);
|
expect(wasAppUninstalled, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
IOSDevice setUpIOSDevice({
|
IOSDevice setUpIOSDevice({
|
||||||
@ -322,6 +393,7 @@ IOSDevice setUpIOSDevice({
|
|||||||
Logger? logger,
|
Logger? logger,
|
||||||
DeviceConnectionInterface? interfaceType,
|
DeviceConnectionInterface? interfaceType,
|
||||||
Artifacts? artifacts,
|
Artifacts? artifacts,
|
||||||
|
bool isCoreDevice = false,
|
||||||
}) {
|
}) {
|
||||||
logger ??= BufferLogger.test();
|
logger ??= BufferLogger.test();
|
||||||
final FakePlatform platform = FakePlatform(
|
final FakePlatform platform = FakePlatform(
|
||||||
@ -357,9 +429,42 @@ IOSDevice setUpIOSDevice({
|
|||||||
artifacts: artifacts,
|
artifacts: artifacts,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
),
|
),
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl(),
|
||||||
|
xcodeDebug: FakeXcodeDebug(),
|
||||||
iProxy: IProxy.test(logger: logger, processManager: processManager),
|
iProxy: IProxy.test(logger: logger, processManager: processManager),
|
||||||
connectionInterface: interfaceType ?? DeviceConnectionInterface.attached,
|
connectionInterface: interfaceType ?? DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: isCoreDevice,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FakeXcodeDebug extends Fake implements XcodeDebug {}
|
||||||
|
|
||||||
|
class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {
|
||||||
|
@override
|
||||||
|
Future<bool> installApp({
|
||||||
|
required String deviceId,
|
||||||
|
required String bundlePath,
|
||||||
|
}) async {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> uninstallApp({
|
||||||
|
required String deviceId,
|
||||||
|
required String bundleId,
|
||||||
|
}) async {
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> isAppInstalled({
|
||||||
|
required String deviceId,
|
||||||
|
required String bundleId,
|
||||||
|
}) async {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -349,9 +349,117 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('both syslog and debugger stream', () {
|
group('Determine which loggers to use', () {
|
||||||
|
testWithoutContext('for physically attached CoreDevice', () {
|
||||||
|
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||||
|
iMobileDevice: IMobileDevice(
|
||||||
|
artifacts: artifacts,
|
||||||
|
processManager: processManager,
|
||||||
|
cache: fakeCache,
|
||||||
|
logger: logger,
|
||||||
|
),
|
||||||
|
majorSdkVersion: 17,
|
||||||
|
isCoreDevice: true,
|
||||||
|
);
|
||||||
|
|
||||||
testWithoutContext('useBothLogDeviceReaders is true when CI option is true and sdk is at least 16', () {
|
expect(logReader.useSyslogLogging, isTrue);
|
||||||
|
expect(logReader.useUnifiedLogging, isTrue);
|
||||||
|
expect(logReader.useIOSDeployLogging, isFalse);
|
||||||
|
expect(logReader.usingMultipleLoggingSources, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('for wirelessly attached CoreDevice', () {
|
||||||
|
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||||
|
iMobileDevice: IMobileDevice(
|
||||||
|
artifacts: artifacts,
|
||||||
|
processManager: processManager,
|
||||||
|
cache: fakeCache,
|
||||||
|
logger: logger,
|
||||||
|
),
|
||||||
|
majorSdkVersion: 17,
|
||||||
|
isCoreDevice: true,
|
||||||
|
isWirelesslyConnected: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logReader.useSyslogLogging, isFalse);
|
||||||
|
expect(logReader.useUnifiedLogging, isTrue);
|
||||||
|
expect(logReader.useIOSDeployLogging, isFalse);
|
||||||
|
expect(logReader.usingMultipleLoggingSources, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('for iOS 12 or less device', () {
|
||||||
|
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||||
|
iMobileDevice: IMobileDevice(
|
||||||
|
artifacts: artifacts,
|
||||||
|
processManager: processManager,
|
||||||
|
cache: fakeCache,
|
||||||
|
logger: logger,
|
||||||
|
),
|
||||||
|
majorSdkVersion: 12,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logReader.useSyslogLogging, isTrue);
|
||||||
|
expect(logReader.useUnifiedLogging, isFalse);
|
||||||
|
expect(logReader.useIOSDeployLogging, isFalse);
|
||||||
|
expect(logReader.usingMultipleLoggingSources, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('for iOS 13 or greater non-CoreDevice', () {
|
||||||
|
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||||
|
iMobileDevice: IMobileDevice(
|
||||||
|
artifacts: artifacts,
|
||||||
|
processManager: processManager,
|
||||||
|
cache: fakeCache,
|
||||||
|
logger: logger,
|
||||||
|
),
|
||||||
|
majorSdkVersion: 13,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logReader.useSyslogLogging, isFalse);
|
||||||
|
expect(logReader.useUnifiedLogging, isTrue);
|
||||||
|
expect(logReader.useIOSDeployLogging, isTrue);
|
||||||
|
expect(logReader.usingMultipleLoggingSources, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('for iOS 13 or greater non-CoreDevice and _iosDeployDebugger is attached', () {
|
||||||
|
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||||
|
iMobileDevice: IMobileDevice(
|
||||||
|
artifacts: artifacts,
|
||||||
|
processManager: processManager,
|
||||||
|
cache: fakeCache,
|
||||||
|
logger: logger,
|
||||||
|
),
|
||||||
|
majorSdkVersion: 13,
|
||||||
|
);
|
||||||
|
|
||||||
|
final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
|
||||||
|
iosDeployDebugger.debuggerAttached = true;
|
||||||
|
logReader.debuggerStream = iosDeployDebugger;
|
||||||
|
|
||||||
|
expect(logReader.useSyslogLogging, isFalse);
|
||||||
|
expect(logReader.useUnifiedLogging, isFalse);
|
||||||
|
expect(logReader.useIOSDeployLogging, isTrue);
|
||||||
|
expect(logReader.usingMultipleLoggingSources, isFalse);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('for iOS 16 or greater non-CoreDevice', () {
|
||||||
|
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||||
|
iMobileDevice: IMobileDevice(
|
||||||
|
artifacts: artifacts,
|
||||||
|
processManager: processManager,
|
||||||
|
cache: fakeCache,
|
||||||
|
logger: logger,
|
||||||
|
),
|
||||||
|
majorSdkVersion: 16,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logReader.useSyslogLogging, isFalse);
|
||||||
|
expect(logReader.useUnifiedLogging, isTrue);
|
||||||
|
expect(logReader.useIOSDeployLogging, isTrue);
|
||||||
|
expect(logReader.usingMultipleLoggingSources, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('for iOS 16 or greater non-CoreDevice in CI', () {
|
||||||
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||||
iMobileDevice: IMobileDevice(
|
iMobileDevice: IMobileDevice(
|
||||||
artifacts: artifacts,
|
artifacts: artifacts,
|
||||||
@ -363,39 +471,13 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
|
|||||||
majorSdkVersion: 16,
|
majorSdkVersion: 16,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(logReader.useBothLogDeviceReaders, isTrue);
|
expect(logReader.useSyslogLogging, isTrue);
|
||||||
|
expect(logReader.useUnifiedLogging, isTrue);
|
||||||
|
expect(logReader.useIOSDeployLogging, isTrue);
|
||||||
|
expect(logReader.usingMultipleLoggingSources, isTrue);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('useBothLogDeviceReaders is false when sdk is less than 16', () {
|
testWithoutContext('syslog sends flutter messages to stream when useSyslogLogging is true', () async {
|
||||||
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
|
||||||
iMobileDevice: IMobileDevice(
|
|
||||||
artifacts: artifacts,
|
|
||||||
processManager: processManager,
|
|
||||||
cache: fakeCache,
|
|
||||||
logger: logger,
|
|
||||||
),
|
|
||||||
usingCISystem: true,
|
|
||||||
majorSdkVersion: 15,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(logReader.useBothLogDeviceReaders, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWithoutContext('useBothLogDeviceReaders is false when CI option is false', () {
|
|
||||||
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
|
||||||
iMobileDevice: IMobileDevice(
|
|
||||||
artifacts: artifacts,
|
|
||||||
processManager: processManager,
|
|
||||||
cache: fakeCache,
|
|
||||||
logger: logger,
|
|
||||||
),
|
|
||||||
majorSdkVersion: 16,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(logReader.useBothLogDeviceReaders, isFalse);
|
|
||||||
});
|
|
||||||
|
|
||||||
testWithoutContext('syslog only sends flutter messages to stream when useBothLogDeviceReaders is true', () async {
|
|
||||||
processManager.addCommand(
|
processManager.addCommand(
|
||||||
FakeCommand(
|
FakeCommand(
|
||||||
command: <String>[
|
command: <String>[
|
||||||
@ -422,7 +504,7 @@ May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextM
|
|||||||
);
|
);
|
||||||
final List<String> lines = await logReader.logLines.toList();
|
final List<String> lines = await logReader.logLines.toList();
|
||||||
|
|
||||||
expect(logReader.useBothLogDeviceReaders, isTrue);
|
expect(logReader.useSyslogLogging, isTrue);
|
||||||
expect(processManager, hasNoRemainingExpectations);
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
expect(lines, <String>[
|
expect(lines, <String>[
|
||||||
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
|
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
|
||||||
@ -430,7 +512,42 @@ May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextM
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('IOSDeviceLogReader uses both syslog and ios-deploy debugger', () async {
|
testWithoutContext('IOSDeviceLogReader only uses ios-deploy debugger when attached and not in CI', () async {
|
||||||
|
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
|
||||||
|
'(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.',
|
||||||
|
'',
|
||||||
|
]);
|
||||||
|
|
||||||
|
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
||||||
|
iMobileDevice: IMobileDevice(
|
||||||
|
artifacts: artifacts,
|
||||||
|
processManager: processManager,
|
||||||
|
cache: fakeCache,
|
||||||
|
logger: logger,
|
||||||
|
),
|
||||||
|
majorSdkVersion: 16,
|
||||||
|
);
|
||||||
|
final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
|
||||||
|
iosDeployDebugger.debuggerAttached = true;
|
||||||
|
iosDeployDebugger.logLines = debuggingLogs;
|
||||||
|
logReader.debuggerStream = iosDeployDebugger;
|
||||||
|
final Future<List<String>> logLines = logReader.logLines.toList();
|
||||||
|
final List<String> lines = await logLines;
|
||||||
|
|
||||||
|
expect(logReader.useIOSDeployLogging, isTrue);
|
||||||
|
expect(logReader.useSyslogLogging, isFalse);
|
||||||
|
expect(logReader.useUnifiedLogging, isFalse);
|
||||||
|
expect(logReader.usingMultipleLoggingSources, isFalse);
|
||||||
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
|
expect(
|
||||||
|
lines.contains(
|
||||||
|
'(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.',
|
||||||
|
),
|
||||||
|
isTrue,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('IOSDeviceLogReader uses both syslog and ios-deploy debugger for CI and filters duplicate messages', () async {
|
||||||
processManager.addCommand(
|
processManager.addCommand(
|
||||||
FakeCommand(
|
FakeCommand(
|
||||||
command: <String>[
|
command: <String>[
|
||||||
@ -465,7 +582,9 @@ May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextM
|
|||||||
final Future<List<String>> logLines = logReader.logLines.toList();
|
final Future<List<String>> logLines = logReader.logLines.toList();
|
||||||
final List<String> lines = await logLines;
|
final List<String> lines = await logLines;
|
||||||
|
|
||||||
expect(logReader.useBothLogDeviceReaders, isTrue);
|
expect(logReader.useSyslogLogging, isTrue);
|
||||||
|
expect(logReader.useIOSDeployLogging, isTrue);
|
||||||
|
expect(logReader.usingMultipleLoggingSources, isTrue);
|
||||||
expect(processManager, hasNoRemainingExpectations);
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
expect(lines.length, 3);
|
expect(lines.length, 3);
|
||||||
expect(lines, containsAll(<String>[
|
expect(lines, containsAll(<String>[
|
||||||
@ -473,38 +592,6 @@ May 30 13:56:28 Runner(Flutter)[2037] <Notice>: [VERBOSE-2:FlutterDarwinContextM
|
|||||||
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
|
'flutter: The Dart VM service is listening on http://127.0.0.1:63098/35ZezGIQLnw=/',
|
||||||
'flutter: Check for duplicate',
|
'flutter: Check for duplicate',
|
||||||
]));
|
]));
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
testWithoutContext('IOSDeviceLogReader only uses ios-deploy debugger when useBothLogDeviceReaders is false', () async {
|
|
||||||
final Stream<String> debuggingLogs = Stream<String>.fromIterable(<String>[
|
|
||||||
'(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.',
|
|
||||||
'',
|
|
||||||
]);
|
|
||||||
|
|
||||||
final IOSDeviceLogReader logReader = IOSDeviceLogReader.test(
|
|
||||||
iMobileDevice: IMobileDevice(
|
|
||||||
artifacts: artifacts,
|
|
||||||
processManager: processManager,
|
|
||||||
cache: fakeCache,
|
|
||||||
logger: logger,
|
|
||||||
),
|
|
||||||
majorSdkVersion: 16,
|
|
||||||
);
|
|
||||||
final FakeIOSDeployDebugger iosDeployDebugger = FakeIOSDeployDebugger();
|
|
||||||
iosDeployDebugger.logLines = debuggingLogs;
|
|
||||||
logReader.debuggerStream = iosDeployDebugger;
|
|
||||||
final Future<List<String>> logLines = logReader.logLines.toList();
|
|
||||||
final List<String> lines = await logLines;
|
|
||||||
|
|
||||||
expect(logReader.useBothLogDeviceReaders, isFalse);
|
|
||||||
expect(processManager, hasNoRemainingExpectations);
|
|
||||||
expect(
|
|
||||||
lines.contains(
|
|
||||||
'(lldb) 2023-05-30 13:48:52.461894-0500 Runner[2019:1101495] [VERBOSE-2:FlutterDarwinContextMetalImpeller.mm(39)] Using the Impeller rendering backend.',
|
|
||||||
),
|
|
||||||
isTrue,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -10,11 +10,14 @@ import 'package:flutter_tools/src/base/platform.dart';
|
|||||||
import 'package:flutter_tools/src/build_info.dart';
|
import 'package:flutter_tools/src/build_info.dart';
|
||||||
import 'package:flutter_tools/src/cache.dart';
|
import 'package:flutter_tools/src/cache.dart';
|
||||||
import 'package:flutter_tools/src/device.dart';
|
import 'package:flutter_tools/src/device.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/core_devices.dart';
|
||||||
import 'package:flutter_tools/src/ios/devices.dart';
|
import 'package:flutter_tools/src/ios/devices.dart';
|
||||||
import 'package:flutter_tools/src/ios/ios_deploy.dart';
|
import 'package:flutter_tools/src/ios/ios_deploy.dart';
|
||||||
import 'package:flutter_tools/src/ios/iproxy.dart';
|
import 'package:flutter_tools/src/ios/iproxy.dart';
|
||||||
import 'package:flutter_tools/src/ios/mac.dart';
|
import 'package:flutter_tools/src/ios/mac.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/xcode_debug.dart';
|
||||||
import 'package:flutter_tools/src/project.dart';
|
import 'package:flutter_tools/src/project.dart';
|
||||||
|
import 'package:test/fake.dart';
|
||||||
|
|
||||||
import '../../src/common.dart';
|
import '../../src/common.dart';
|
||||||
import '../../src/context.dart';
|
import '../../src/context.dart';
|
||||||
@ -94,6 +97,8 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) {
|
|||||||
cache: Cache.test(processManager: processManager),
|
cache: Cache.test(processManager: processManager),
|
||||||
),
|
),
|
||||||
iMobileDevice: IMobileDevice.test(processManager: processManager),
|
iMobileDevice: IMobileDevice.test(processManager: processManager),
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl(),
|
||||||
|
xcodeDebug: FakeXcodeDebug(),
|
||||||
platform: platform,
|
platform: platform,
|
||||||
name: 'iPhone 1',
|
name: 'iPhone 1',
|
||||||
sdkVersion: '13.3',
|
sdkVersion: '13.3',
|
||||||
@ -102,5 +107,10 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) {
|
|||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: false,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FakeXcodeDebug extends Fake implements XcodeDebug {}
|
||||||
|
|
||||||
|
class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:fake_async/fake_async.dart';
|
import 'package:fake_async/fake_async.dart';
|
||||||
import 'package:file/memory.dart';
|
import 'package:file/memory.dart';
|
||||||
import 'package:file_testing/file_testing.dart';
|
import 'package:file_testing/file_testing.dart';
|
||||||
@ -13,11 +15,14 @@ import 'package:flutter_tools/src/base/version.dart';
|
|||||||
import 'package:flutter_tools/src/build_info.dart';
|
import 'package:flutter_tools/src/build_info.dart';
|
||||||
import 'package:flutter_tools/src/cache.dart';
|
import 'package:flutter_tools/src/cache.dart';
|
||||||
import 'package:flutter_tools/src/device.dart';
|
import 'package:flutter_tools/src/device.dart';
|
||||||
|
import 'package:flutter_tools/src/device_port_forwarder.dart';
|
||||||
import 'package:flutter_tools/src/ios/application_package.dart';
|
import 'package:flutter_tools/src/ios/application_package.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/core_devices.dart';
|
||||||
import 'package:flutter_tools/src/ios/devices.dart';
|
import 'package:flutter_tools/src/ios/devices.dart';
|
||||||
import 'package:flutter_tools/src/ios/ios_deploy.dart';
|
import 'package:flutter_tools/src/ios/ios_deploy.dart';
|
||||||
import 'package:flutter_tools/src/ios/iproxy.dart';
|
import 'package:flutter_tools/src/ios/iproxy.dart';
|
||||||
import 'package:flutter_tools/src/ios/mac.dart';
|
import 'package:flutter_tools/src/ios/mac.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/xcode_debug.dart';
|
||||||
import 'package:flutter_tools/src/ios/xcodeproj.dart';
|
import 'package:flutter_tools/src/ios/xcodeproj.dart';
|
||||||
import 'package:flutter_tools/src/macos/xcode.dart';
|
import 'package:flutter_tools/src/macos/xcode.dart';
|
||||||
import 'package:flutter_tools/src/project.dart';
|
import 'package:flutter_tools/src/project.dart';
|
||||||
@ -25,6 +30,7 @@ import 'package:test/fake.dart';
|
|||||||
|
|
||||||
import '../../src/common.dart';
|
import '../../src/common.dart';
|
||||||
import '../../src/context.dart' hide FakeXcodeProjectInterpreter;
|
import '../../src/context.dart' hide FakeXcodeProjectInterpreter;
|
||||||
|
import '../../src/fake_devices.dart';
|
||||||
import '../../src/fake_process_manager.dart';
|
import '../../src/fake_process_manager.dart';
|
||||||
import '../../src/fakes.dart';
|
import '../../src/fakes.dart';
|
||||||
|
|
||||||
@ -287,13 +293,363 @@ void main() {
|
|||||||
Xcode: () => xcode,
|
Xcode: () => xcode,
|
||||||
}, skip: true); // TODO(zanderso): clean up with https://github.com/flutter/flutter/issues/60675
|
}, skip: true); // TODO(zanderso): clean up with https://github.com/flutter/flutter/issues/60675
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('IOSDevice.startApp for CoreDevice', () {
|
||||||
|
late FileSystem fileSystem;
|
||||||
|
late FakeProcessManager processManager;
|
||||||
|
late BufferLogger logger;
|
||||||
|
late Xcode xcode;
|
||||||
|
late FakeXcodeProjectInterpreter fakeXcodeProjectInterpreter;
|
||||||
|
late XcodeProjectInfo projectInfo;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
logger = BufferLogger.test();
|
||||||
|
fileSystem = MemoryFileSystem.test();
|
||||||
|
processManager = FakeProcessManager.empty();
|
||||||
|
projectInfo = XcodeProjectInfo(
|
||||||
|
<String>['Runner'],
|
||||||
|
<String>['Debug', 'Release'],
|
||||||
|
<String>['Runner'],
|
||||||
|
logger,
|
||||||
|
);
|
||||||
|
fakeXcodeProjectInterpreter = FakeXcodeProjectInterpreter(projectInfo: projectInfo);
|
||||||
|
xcode = Xcode.test(processManager: FakeProcessManager.any(), xcodeProjectInterpreter: fakeXcodeProjectInterpreter);
|
||||||
|
fileSystem.file('foo/.packages')
|
||||||
|
..createSync(recursive: true)
|
||||||
|
..writeAsStringSync('\n');
|
||||||
|
});
|
||||||
|
|
||||||
|
group('in release mode', () {
|
||||||
|
testUsingContext('suceeds when install and launch succeed', () async {
|
||||||
|
final IOSDevice iosDevice = setUpIOSDevice(
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
processManager: FakeProcessManager.any(),
|
||||||
|
logger: logger,
|
||||||
|
artifacts: artifacts,
|
||||||
|
isCoreDevice: true,
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl(),
|
||||||
|
);
|
||||||
|
setUpIOSProject(fileSystem);
|
||||||
|
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
|
||||||
|
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
|
||||||
|
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
|
||||||
|
|
||||||
|
final LaunchResult launchResult = await iosDevice.startApp(
|
||||||
|
buildableIOSApp,
|
||||||
|
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
|
||||||
|
platformArgs: <String, Object>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fileSystem.directory('build/ios/iphoneos'), exists);
|
||||||
|
expect(launchResult.started, true);
|
||||||
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
|
Platform: () => macPlatform,
|
||||||
|
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
|
||||||
|
Xcode: () => xcode,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('fails when install fails', () async {
|
||||||
|
final IOSDevice iosDevice = setUpIOSDevice(
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
processManager: FakeProcessManager.any(),
|
||||||
|
logger: logger,
|
||||||
|
artifacts: artifacts,
|
||||||
|
isCoreDevice: true,
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl(
|
||||||
|
installSuccess: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setUpIOSProject(fileSystem);
|
||||||
|
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
|
||||||
|
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
|
||||||
|
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
|
||||||
|
|
||||||
|
final LaunchResult launchResult = await iosDevice.startApp(
|
||||||
|
buildableIOSApp,
|
||||||
|
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
|
||||||
|
platformArgs: <String, Object>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fileSystem.directory('build/ios/iphoneos'), exists);
|
||||||
|
expect(launchResult.started, false);
|
||||||
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
|
Platform: () => macPlatform,
|
||||||
|
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
|
||||||
|
Xcode: () => xcode,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('fails when launch fails', () async {
|
||||||
|
final IOSDevice iosDevice = setUpIOSDevice(
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
processManager: FakeProcessManager.any(),
|
||||||
|
logger: logger,
|
||||||
|
artifacts: artifacts,
|
||||||
|
isCoreDevice: true,
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl(
|
||||||
|
launchSuccess: false,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
setUpIOSProject(fileSystem);
|
||||||
|
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
|
||||||
|
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
|
||||||
|
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
|
||||||
|
|
||||||
|
final LaunchResult launchResult = await iosDevice.startApp(
|
||||||
|
buildableIOSApp,
|
||||||
|
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
|
||||||
|
platformArgs: <String, Object>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fileSystem.directory('build/ios/iphoneos'), exists);
|
||||||
|
expect(launchResult.started, false);
|
||||||
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
|
Platform: () => macPlatform,
|
||||||
|
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
|
||||||
|
Xcode: () => xcode,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('ensure arguments passed to launch', () async {
|
||||||
|
final FakeIOSCoreDeviceControl coreDeviceControl = FakeIOSCoreDeviceControl();
|
||||||
|
final IOSDevice iosDevice = setUpIOSDevice(
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
processManager: FakeProcessManager.any(),
|
||||||
|
logger: logger,
|
||||||
|
artifacts: artifacts,
|
||||||
|
isCoreDevice: true,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
);
|
||||||
|
setUpIOSProject(fileSystem);
|
||||||
|
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
|
||||||
|
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
|
||||||
|
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
|
||||||
|
|
||||||
|
final LaunchResult launchResult = await iosDevice.startApp(
|
||||||
|
buildableIOSApp,
|
||||||
|
debuggingOptions: DebuggingOptions.disabled(BuildInfo.release),
|
||||||
|
platformArgs: <String, Object>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(fileSystem.directory('build/ios/iphoneos'), exists);
|
||||||
|
expect(launchResult.started, true);
|
||||||
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
|
expect(coreDeviceControl.argumentsUsedForLaunch, isNotNull);
|
||||||
|
expect(coreDeviceControl.argumentsUsedForLaunch, contains('--enable-dart-profiling'));
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
|
Platform: () => macPlatform,
|
||||||
|
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
|
||||||
|
Xcode: () => xcode,
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
group('in debug mode', () {
|
||||||
|
|
||||||
|
testUsingContext('succeeds', () async {
|
||||||
|
final IOSDevice iosDevice = setUpIOSDevice(
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
processManager: FakeProcessManager.any(),
|
||||||
|
logger: logger,
|
||||||
|
artifacts: artifacts,
|
||||||
|
isCoreDevice: true,
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl(),
|
||||||
|
xcodeDebug: FakeXcodeDebug(
|
||||||
|
expectedProject: XcodeDebugProject(
|
||||||
|
scheme: 'Runner',
|
||||||
|
xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
|
||||||
|
xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
|
||||||
|
),
|
||||||
|
expectedDeviceId: '123',
|
||||||
|
expectedLaunchArguments: <String>['--enable-dart-profiling'],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
setUpIOSProject(fileSystem);
|
||||||
|
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
|
||||||
|
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
|
||||||
|
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
|
||||||
|
|
||||||
|
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
|
||||||
|
|
||||||
|
iosDevice.portForwarder = const NoOpDevicePortForwarder();
|
||||||
|
iosDevice.setLogReader(buildableIOSApp, deviceLogReader);
|
||||||
|
|
||||||
|
// Start writing messages to the log reader.
|
||||||
|
Timer.run(() {
|
||||||
|
deviceLogReader.addLine('Foo');
|
||||||
|
deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
|
||||||
|
});
|
||||||
|
|
||||||
|
final LaunchResult launchResult = await iosDevice.startApp(
|
||||||
|
buildableIOSApp,
|
||||||
|
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
|
||||||
|
BuildMode.debug,
|
||||||
|
null,
|
||||||
|
buildName: '1.2.3',
|
||||||
|
buildNumber: '4',
|
||||||
|
treeShakeIcons: false,
|
||||||
|
)),
|
||||||
|
platformArgs: <String, Object>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(logger.errorText, isEmpty);
|
||||||
|
expect(fileSystem.directory('build/ios/iphoneos'), exists);
|
||||||
|
expect(launchResult.started, true);
|
||||||
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
|
Platform: () => macPlatform,
|
||||||
|
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
|
||||||
|
Xcode: () => xcode,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('fails when Xcode project is not found', () async {
|
||||||
|
final IOSDevice iosDevice = setUpIOSDevice(
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
processManager: FakeProcessManager.any(),
|
||||||
|
logger: logger,
|
||||||
|
artifacts: artifacts,
|
||||||
|
isCoreDevice: true,
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl()
|
||||||
|
);
|
||||||
|
setUpIOSProject(fileSystem);
|
||||||
|
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
|
||||||
|
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
|
||||||
|
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
|
||||||
|
|
||||||
|
final LaunchResult launchResult = await iosDevice.startApp(
|
||||||
|
buildableIOSApp,
|
||||||
|
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
|
||||||
|
BuildMode.debug,
|
||||||
|
null,
|
||||||
|
buildName: '1.2.3',
|
||||||
|
buildNumber: '4',
|
||||||
|
treeShakeIcons: false,
|
||||||
|
)),
|
||||||
|
platformArgs: <String, Object>{},
|
||||||
|
);
|
||||||
|
expect(logger.errorText, contains('Xcode project not found'));
|
||||||
|
expect(fileSystem.directory('build/ios/iphoneos'), exists);
|
||||||
|
expect(launchResult.started, false);
|
||||||
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
|
Platform: () => macPlatform,
|
||||||
|
XcodeProjectInterpreter: () => FakeXcodeProjectInterpreter(),
|
||||||
|
Xcode: () => xcode,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('fails when Xcode workspace is not found', () async {
|
||||||
|
final IOSDevice iosDevice = setUpIOSDevice(
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
processManager: FakeProcessManager.any(),
|
||||||
|
logger: logger,
|
||||||
|
artifacts: artifacts,
|
||||||
|
isCoreDevice: true,
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl()
|
||||||
|
);
|
||||||
|
setUpIOSProject(fileSystem, createWorkspace: false);
|
||||||
|
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
|
||||||
|
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
|
||||||
|
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
|
||||||
|
|
||||||
|
final LaunchResult launchResult = await iosDevice.startApp(
|
||||||
|
buildableIOSApp,
|
||||||
|
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
|
||||||
|
BuildMode.debug,
|
||||||
|
null,
|
||||||
|
buildName: '1.2.3',
|
||||||
|
buildNumber: '4',
|
||||||
|
treeShakeIcons: false,
|
||||||
|
)),
|
||||||
|
platformArgs: <String, Object>{},
|
||||||
|
);
|
||||||
|
expect(logger.errorText, contains('Unable to get Xcode workspace'));
|
||||||
|
expect(fileSystem.directory('build/ios/iphoneos'), exists);
|
||||||
|
expect(launchResult.started, false);
|
||||||
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
|
Platform: () => macPlatform,
|
||||||
|
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
|
||||||
|
Xcode: () => xcode,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('fails when scheme is not found', () async {
|
||||||
|
final IOSDevice iosDevice = setUpIOSDevice(
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
processManager: FakeProcessManager.any(),
|
||||||
|
logger: logger,
|
||||||
|
artifacts: artifacts,
|
||||||
|
isCoreDevice: true,
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl()
|
||||||
|
);
|
||||||
|
setUpIOSProject(fileSystem);
|
||||||
|
final FlutterProject flutterProject = FlutterProject.fromDirectory(fileSystem.currentDirectory);
|
||||||
|
final BuildableIOSApp buildableIOSApp = BuildableIOSApp(flutterProject.ios, 'flutter', 'My Super Awesome App.app');
|
||||||
|
fileSystem.directory('build/ios/Release-iphoneos/My Super Awesome App.app').createSync(recursive: true);
|
||||||
|
|
||||||
|
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
|
||||||
|
|
||||||
|
iosDevice.portForwarder = const NoOpDevicePortForwarder();
|
||||||
|
iosDevice.setLogReader(buildableIOSApp, deviceLogReader);
|
||||||
|
|
||||||
|
// Start writing messages to the log reader.
|
||||||
|
Timer.run(() {
|
||||||
|
deviceLogReader.addLine('Foo');
|
||||||
|
deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(() async => iosDevice.startApp(
|
||||||
|
buildableIOSApp,
|
||||||
|
debuggingOptions: DebuggingOptions.enabled(const BuildInfo(
|
||||||
|
BuildMode.debug,
|
||||||
|
'Flavor',
|
||||||
|
buildName: '1.2.3',
|
||||||
|
buildNumber: '4',
|
||||||
|
treeShakeIcons: false,
|
||||||
|
)),
|
||||||
|
platformArgs: <String, Object>{},
|
||||||
|
), throwsToolExit());
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
FileSystem: () => fileSystem,
|
||||||
|
Logger: () => logger,
|
||||||
|
Platform: () => macPlatform,
|
||||||
|
XcodeProjectInterpreter: () => fakeXcodeProjectInterpreter,
|
||||||
|
Xcode: () => xcode,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void setUpIOSProject(FileSystem fileSystem) {
|
void setUpIOSProject(FileSystem fileSystem, {bool createWorkspace = true}) {
|
||||||
fileSystem.file('pubspec.yaml').createSync();
|
fileSystem.file('pubspec.yaml').createSync();
|
||||||
fileSystem.file('.packages').writeAsStringSync('\n');
|
fileSystem.file('.packages').writeAsStringSync('\n');
|
||||||
fileSystem.directory('ios').createSync();
|
fileSystem.directory('ios').createSync();
|
||||||
fileSystem.directory('ios/Runner.xcworkspace').createSync();
|
if (createWorkspace) {
|
||||||
|
fileSystem.directory('ios/Runner.xcworkspace').createSync();
|
||||||
|
}
|
||||||
fileSystem.file('ios/Runner.xcodeproj/project.pbxproj').createSync(recursive: true);
|
fileSystem.file('ios/Runner.xcodeproj/project.pbxproj').createSync(recursive: true);
|
||||||
// This is the expected output directory.
|
// This is the expected output directory.
|
||||||
fileSystem.directory('build/ios/iphoneos/My Super Awesome App.app').createSync(recursive: true);
|
fileSystem.directory('build/ios/iphoneos/My Super Awesome App.app').createSync(recursive: true);
|
||||||
@ -305,6 +661,9 @@ IOSDevice setUpIOSDevice({
|
|||||||
Logger? logger,
|
Logger? logger,
|
||||||
ProcessManager? processManager,
|
ProcessManager? processManager,
|
||||||
Artifacts? artifacts,
|
Artifacts? artifacts,
|
||||||
|
bool isCoreDevice = false,
|
||||||
|
IOSCoreDeviceControl? coreDeviceControl,
|
||||||
|
FakeXcodeDebug? xcodeDebug,
|
||||||
}) {
|
}) {
|
||||||
artifacts ??= Artifacts.test();
|
artifacts ??= Artifacts.test();
|
||||||
final Cache cache = Cache.test(
|
final Cache cache = Cache.test(
|
||||||
@ -336,10 +695,13 @@ IOSDevice setUpIOSDevice({
|
|||||||
artifacts: artifacts,
|
artifacts: artifacts,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
),
|
),
|
||||||
|
coreDeviceControl: coreDeviceControl ?? FakeIOSCoreDeviceControl(),
|
||||||
|
xcodeDebug: xcodeDebug ?? FakeXcodeDebug(),
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
connectionInterface: DeviceConnectionInterface.attached,
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: isCoreDevice,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -381,3 +743,70 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete
|
|||||||
Duration timeout = const Duration(minutes: 1),
|
Duration timeout = const Duration(minutes: 1),
|
||||||
}) async => buildSettings;
|
}) async => buildSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FakeXcodeDebug extends Fake implements XcodeDebug {
|
||||||
|
FakeXcodeDebug({
|
||||||
|
this.debugSuccess = true,
|
||||||
|
this.expectedProject,
|
||||||
|
this.expectedDeviceId,
|
||||||
|
this.expectedLaunchArguments,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool debugSuccess;
|
||||||
|
|
||||||
|
final XcodeDebugProject? expectedProject;
|
||||||
|
final String? expectedDeviceId;
|
||||||
|
final List<String>? expectedLaunchArguments;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> debugApp({
|
||||||
|
required XcodeDebugProject project,
|
||||||
|
required String deviceId,
|
||||||
|
required List<String> launchArguments,
|
||||||
|
}) async {
|
||||||
|
if (expectedProject != null) {
|
||||||
|
expect(project.scheme, expectedProject!.scheme);
|
||||||
|
expect(project.xcodeWorkspace.path, expectedProject!.xcodeWorkspace.path);
|
||||||
|
expect(project.xcodeProject.path, expectedProject!.xcodeProject.path);
|
||||||
|
expect(project.isTemporaryProject, expectedProject!.isTemporaryProject);
|
||||||
|
}
|
||||||
|
if (expectedDeviceId != null) {
|
||||||
|
expect(deviceId, expectedDeviceId);
|
||||||
|
}
|
||||||
|
if (expectedLaunchArguments != null) {
|
||||||
|
expect(expectedLaunchArguments, launchArguments);
|
||||||
|
}
|
||||||
|
return debugSuccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {
|
||||||
|
FakeIOSCoreDeviceControl({
|
||||||
|
this.installSuccess = true,
|
||||||
|
this.launchSuccess = true
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool installSuccess;
|
||||||
|
final bool launchSuccess;
|
||||||
|
List<String>? _launchArguments;
|
||||||
|
|
||||||
|
List<String>? get argumentsUsedForLaunch => _launchArguments;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> installApp({
|
||||||
|
required String deviceId,
|
||||||
|
required String bundlePath,
|
||||||
|
}) async {
|
||||||
|
return installSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> launchApp({
|
||||||
|
required String deviceId,
|
||||||
|
required String bundleId,
|
||||||
|
List<String> launchArguments = const <String>[],
|
||||||
|
}) async {
|
||||||
|
_launchArguments = launchArguments;
|
||||||
|
return launchSuccess;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -5,20 +5,25 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:fake_async/fake_async.dart';
|
||||||
import 'package:file/memory.dart';
|
import 'package:file/memory.dart';
|
||||||
import 'package:flutter_tools/src/artifacts.dart';
|
import 'package:flutter_tools/src/artifacts.dart';
|
||||||
import 'package:flutter_tools/src/base/file_system.dart';
|
import 'package:flutter_tools/src/base/file_system.dart';
|
||||||
import 'package:flutter_tools/src/base/logger.dart';
|
import 'package:flutter_tools/src/base/logger.dart';
|
||||||
import 'package:flutter_tools/src/base/platform.dart';
|
import 'package:flutter_tools/src/base/platform.dart';
|
||||||
|
import 'package:flutter_tools/src/base/process.dart';
|
||||||
|
import 'package:flutter_tools/src/base/template.dart';
|
||||||
import 'package:flutter_tools/src/build_info.dart';
|
import 'package:flutter_tools/src/build_info.dart';
|
||||||
import 'package:flutter_tools/src/cache.dart';
|
import 'package:flutter_tools/src/cache.dart';
|
||||||
import 'package:flutter_tools/src/device.dart';
|
import 'package:flutter_tools/src/device.dart';
|
||||||
import 'package:flutter_tools/src/device_port_forwarder.dart';
|
import 'package:flutter_tools/src/device_port_forwarder.dart';
|
||||||
import 'package:flutter_tools/src/ios/application_package.dart';
|
import 'package:flutter_tools/src/ios/application_package.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/core_devices.dart';
|
||||||
import 'package:flutter_tools/src/ios/devices.dart';
|
import 'package:flutter_tools/src/ios/devices.dart';
|
||||||
import 'package:flutter_tools/src/ios/ios_deploy.dart';
|
import 'package:flutter_tools/src/ios/ios_deploy.dart';
|
||||||
import 'package:flutter_tools/src/ios/iproxy.dart';
|
import 'package:flutter_tools/src/ios/iproxy.dart';
|
||||||
import 'package:flutter_tools/src/ios/mac.dart';
|
import 'package:flutter_tools/src/ios/mac.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/xcode_debug.dart';
|
||||||
import 'package:flutter_tools/src/mdns_discovery.dart';
|
import 'package:flutter_tools/src/mdns_discovery.dart';
|
||||||
import 'package:test/fake.dart';
|
import 'package:test/fake.dart';
|
||||||
|
|
||||||
@ -601,6 +606,212 @@ void main() {
|
|||||||
expect(await device.stopApp(iosApp), false);
|
expect(await device.stopApp(iosApp), false);
|
||||||
expect(processManager, hasNoRemainingExpectations);
|
expect(processManager, hasNoRemainingExpectations);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('IOSDevice.startApp for CoreDevice', () {
|
||||||
|
group('in debug mode', () {
|
||||||
|
testUsingContext('succeeds', () async {
|
||||||
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
||||||
|
final FakeProcessManager processManager = FakeProcessManager.empty();
|
||||||
|
|
||||||
|
final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory.childDirectory('flutter_empty_xcode.rand0');
|
||||||
|
final Directory bundleLocation = fileSystem.currentDirectory;
|
||||||
|
final IOSDevice device = setUpIOSDevice(
|
||||||
|
processManager: processManager,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
isCoreDevice: true,
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl(),
|
||||||
|
xcodeDebug: FakeXcodeDebug(
|
||||||
|
expectedProject: XcodeDebugProject(
|
||||||
|
scheme: 'Runner',
|
||||||
|
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
|
||||||
|
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
|
||||||
|
),
|
||||||
|
expectedDeviceId: '123',
|
||||||
|
expectedLaunchArguments: <String>['--enable-dart-profiling'],
|
||||||
|
expectedBundlePath: bundleLocation.path,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
final IOSApp iosApp = PrebuiltIOSApp(
|
||||||
|
projectBundleId: 'app',
|
||||||
|
bundleName: 'Runner',
|
||||||
|
uncompressedBundle: bundleLocation,
|
||||||
|
applicationPackage: bundleLocation,
|
||||||
|
);
|
||||||
|
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
|
||||||
|
|
||||||
|
device.portForwarder = const NoOpDevicePortForwarder();
|
||||||
|
device.setLogReader(iosApp, deviceLogReader);
|
||||||
|
|
||||||
|
// Start writing messages to the log reader.
|
||||||
|
Timer.run(() {
|
||||||
|
deviceLogReader.addLine('Foo');
|
||||||
|
deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
|
||||||
|
});
|
||||||
|
|
||||||
|
final LaunchResult launchResult = await device.startApp(iosApp,
|
||||||
|
prebuiltApplication: true,
|
||||||
|
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
|
||||||
|
platformArgs: <String, dynamic>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(launchResult.started, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('prints warning message if it takes too long to start debugging', () async {
|
||||||
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
||||||
|
final FakeProcessManager processManager = FakeProcessManager.empty();
|
||||||
|
final BufferLogger logger = BufferLogger.test();
|
||||||
|
final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory.childDirectory('flutter_empty_xcode.rand0');
|
||||||
|
final Directory bundleLocation = fileSystem.currentDirectory;
|
||||||
|
final Completer<void> completer = Completer<void>();
|
||||||
|
final FakeXcodeDebug xcodeDebug = FakeXcodeDebug(
|
||||||
|
expectedProject: XcodeDebugProject(
|
||||||
|
scheme: 'Runner',
|
||||||
|
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
|
||||||
|
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
|
||||||
|
),
|
||||||
|
expectedDeviceId: '123',
|
||||||
|
expectedLaunchArguments: <String>['--enable-dart-profiling'],
|
||||||
|
expectedBundlePath: bundleLocation.path,
|
||||||
|
completer: completer,
|
||||||
|
);
|
||||||
|
final IOSDevice device = setUpIOSDevice(
|
||||||
|
processManager: processManager,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
logger: logger,
|
||||||
|
isCoreDevice: true,
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl(),
|
||||||
|
xcodeDebug: xcodeDebug,
|
||||||
|
);
|
||||||
|
final IOSApp iosApp = PrebuiltIOSApp(
|
||||||
|
projectBundleId: 'app',
|
||||||
|
bundleName: 'Runner',
|
||||||
|
uncompressedBundle: bundleLocation,
|
||||||
|
applicationPackage: bundleLocation,
|
||||||
|
);
|
||||||
|
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
|
||||||
|
|
||||||
|
device.portForwarder = const NoOpDevicePortForwarder();
|
||||||
|
device.setLogReader(iosApp, deviceLogReader);
|
||||||
|
|
||||||
|
// Start writing messages to the log reader.
|
||||||
|
Timer.run(() {
|
||||||
|
deviceLogReader.addLine('Foo');
|
||||||
|
deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
|
||||||
|
});
|
||||||
|
|
||||||
|
FakeAsync().run((FakeAsync fakeAsync) {
|
||||||
|
device.startApp(iosApp,
|
||||||
|
prebuiltApplication: true,
|
||||||
|
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
|
||||||
|
platformArgs: <String, dynamic>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
fakeAsync.flushTimers();
|
||||||
|
expect(logger.errorText, contains('Xcode is taking longer than expected to start debugging the app. Ensure the project is opened in Xcode.'));
|
||||||
|
completer.complete();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('succeeds with shutdown hook added when running from CI', () async {
|
||||||
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
||||||
|
final FakeProcessManager processManager = FakeProcessManager.empty();
|
||||||
|
|
||||||
|
final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory.childDirectory('flutter_empty_xcode.rand0');
|
||||||
|
final Directory bundleLocation = fileSystem.currentDirectory;
|
||||||
|
final IOSDevice device = setUpIOSDevice(
|
||||||
|
processManager: processManager,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
isCoreDevice: true,
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl(),
|
||||||
|
xcodeDebug: FakeXcodeDebug(
|
||||||
|
expectedProject: XcodeDebugProject(
|
||||||
|
scheme: 'Runner',
|
||||||
|
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
|
||||||
|
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
|
||||||
|
),
|
||||||
|
expectedDeviceId: '123',
|
||||||
|
expectedLaunchArguments: <String>['--enable-dart-profiling'],
|
||||||
|
expectedBundlePath: bundleLocation.path,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
final IOSApp iosApp = PrebuiltIOSApp(
|
||||||
|
projectBundleId: 'app',
|
||||||
|
bundleName: 'Runner',
|
||||||
|
uncompressedBundle: bundleLocation,
|
||||||
|
applicationPackage: bundleLocation,
|
||||||
|
);
|
||||||
|
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
|
||||||
|
|
||||||
|
device.portForwarder = const NoOpDevicePortForwarder();
|
||||||
|
device.setLogReader(iosApp, deviceLogReader);
|
||||||
|
|
||||||
|
// Start writing messages to the log reader.
|
||||||
|
Timer.run(() {
|
||||||
|
deviceLogReader.addLine('Foo');
|
||||||
|
deviceLogReader.addLine('The Dart VM service is listening on http://127.0.0.1:456');
|
||||||
|
});
|
||||||
|
|
||||||
|
final FakeShutDownHooks shutDownHooks = FakeShutDownHooks();
|
||||||
|
|
||||||
|
final LaunchResult launchResult = await device.startApp(iosApp,
|
||||||
|
prebuiltApplication: true,
|
||||||
|
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug, usingCISystem: true),
|
||||||
|
platformArgs: <String, dynamic>{},
|
||||||
|
shutdownHooks: shutDownHooks,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(launchResult.started, true);
|
||||||
|
expect(shutDownHooks.hooks.length, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('IOSDevice.startApp attaches in debug mode via mDNS when device logging fails', () async {
|
||||||
|
final FileSystem fileSystem = MemoryFileSystem.test();
|
||||||
|
final FakeProcessManager processManager = FakeProcessManager.empty();
|
||||||
|
|
||||||
|
final Directory temporaryXcodeProjectDirectory = fileSystem.systemTempDirectory.childDirectory('flutter_empty_xcode.rand0');
|
||||||
|
final Directory bundleLocation = fileSystem.currentDirectory;
|
||||||
|
final IOSDevice device = setUpIOSDevice(
|
||||||
|
processManager: processManager,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
isCoreDevice: true,
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl(),
|
||||||
|
xcodeDebug: FakeXcodeDebug(
|
||||||
|
expectedProject: XcodeDebugProject(
|
||||||
|
scheme: 'Runner',
|
||||||
|
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
|
||||||
|
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
|
||||||
|
),
|
||||||
|
expectedDeviceId: '123',
|
||||||
|
expectedLaunchArguments: <String>['--enable-dart-profiling'],
|
||||||
|
expectedBundlePath: bundleLocation.path,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
final IOSApp iosApp = PrebuiltIOSApp(
|
||||||
|
projectBundleId: 'app',
|
||||||
|
bundleName: 'Runner',
|
||||||
|
uncompressedBundle: bundleLocation,
|
||||||
|
applicationPackage: bundleLocation,
|
||||||
|
);
|
||||||
|
final FakeDeviceLogReader deviceLogReader = FakeDeviceLogReader();
|
||||||
|
|
||||||
|
device.portForwarder = const NoOpDevicePortForwarder();
|
||||||
|
device.setLogReader(iosApp, deviceLogReader);
|
||||||
|
|
||||||
|
final LaunchResult launchResult = await device.startApp(iosApp,
|
||||||
|
prebuiltApplication: true,
|
||||||
|
debuggingOptions: DebuggingOptions.enabled(BuildInfo.debug),
|
||||||
|
platformArgs: <String, dynamic>{},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(launchResult.started, true);
|
||||||
|
expect(launchResult.hasVmService, true);
|
||||||
|
expect(await device.stopApp(iosApp), true);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
MDnsVmServiceDiscovery: () => FakeMDnsVmServiceDiscovery(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
IOSDevice setUpIOSDevice({
|
IOSDevice setUpIOSDevice({
|
||||||
@ -610,6 +821,9 @@ IOSDevice setUpIOSDevice({
|
|||||||
ProcessManager? processManager,
|
ProcessManager? processManager,
|
||||||
IOSDeploy? iosDeploy,
|
IOSDeploy? iosDeploy,
|
||||||
DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached,
|
DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached,
|
||||||
|
bool isCoreDevice = false,
|
||||||
|
IOSCoreDeviceControl? coreDeviceControl,
|
||||||
|
FakeXcodeDebug? xcodeDebug,
|
||||||
}) {
|
}) {
|
||||||
final Artifacts artifacts = Artifacts.test();
|
final Artifacts artifacts = Artifacts.test();
|
||||||
final FakePlatform macPlatform = FakePlatform(
|
final FakePlatform macPlatform = FakePlatform(
|
||||||
@ -646,10 +860,13 @@ IOSDevice setUpIOSDevice({
|
|||||||
artifacts: artifacts,
|
artifacts: artifacts,
|
||||||
cache: cache,
|
cache: cache,
|
||||||
),
|
),
|
||||||
|
coreDeviceControl: coreDeviceControl ?? FakeIOSCoreDeviceControl(),
|
||||||
|
xcodeDebug: xcodeDebug ?? FakeXcodeDebug(),
|
||||||
cpuArchitecture: DarwinArch.arm64,
|
cpuArchitecture: DarwinArch.arm64,
|
||||||
connectionInterface: interfaceType,
|
connectionInterface: interfaceType,
|
||||||
isConnected: true,
|
isConnected: true,
|
||||||
devModeEnabled: true,
|
devModeEnabled: true,
|
||||||
|
isCoreDevice: isCoreDevice,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -669,10 +886,88 @@ class FakeMDnsVmServiceDiscovery extends Fake implements MDnsVmServiceDiscovery
|
|||||||
Device device, {
|
Device device, {
|
||||||
bool usesIpv6 = false,
|
bool usesIpv6 = false,
|
||||||
int? hostVmservicePort,
|
int? hostVmservicePort,
|
||||||
required int deviceVmservicePort,
|
int? deviceVmservicePort,
|
||||||
bool useDeviceIPAsHost = false,
|
bool useDeviceIPAsHost = false,
|
||||||
Duration timeout = Duration.zero,
|
Duration timeout = Duration.zero,
|
||||||
}) async {
|
}) async {
|
||||||
return Uri.tryParse('http://0.0.0.0:1234');
|
return Uri.tryParse('http://0.0.0.0:1234');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FakeXcodeDebug extends Fake implements XcodeDebug {
|
||||||
|
FakeXcodeDebug({
|
||||||
|
this.debugSuccess = true,
|
||||||
|
this.expectedProject,
|
||||||
|
this.expectedDeviceId,
|
||||||
|
this.expectedLaunchArguments,
|
||||||
|
this.expectedBundlePath,
|
||||||
|
this.completer,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool debugSuccess;
|
||||||
|
final XcodeDebugProject? expectedProject;
|
||||||
|
final String? expectedDeviceId;
|
||||||
|
final List<String>? expectedLaunchArguments;
|
||||||
|
final String? expectedBundlePath;
|
||||||
|
final Completer<void>? completer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool debugStarted = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<XcodeDebugProject> createXcodeProjectWithCustomBundle(
|
||||||
|
String deviceBundlePath, {
|
||||||
|
required TemplateRenderer templateRenderer,
|
||||||
|
Directory? projectDestination,
|
||||||
|
bool verboseLogging = false,
|
||||||
|
}) async {
|
||||||
|
if (expectedBundlePath != null) {
|
||||||
|
expect(expectedBundlePath, deviceBundlePath);
|
||||||
|
}
|
||||||
|
return expectedProject!;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> debugApp({
|
||||||
|
required XcodeDebugProject project,
|
||||||
|
required String deviceId,
|
||||||
|
required List<String> launchArguments,
|
||||||
|
}) async {
|
||||||
|
if (expectedProject != null) {
|
||||||
|
expect(project.scheme, expectedProject!.scheme);
|
||||||
|
expect(project.xcodeWorkspace.path, expectedProject!.xcodeWorkspace.path);
|
||||||
|
expect(project.xcodeProject.path, expectedProject!.xcodeProject.path);
|
||||||
|
expect(project.isTemporaryProject, expectedProject!.isTemporaryProject);
|
||||||
|
}
|
||||||
|
if (expectedDeviceId != null) {
|
||||||
|
expect(deviceId, expectedDeviceId);
|
||||||
|
}
|
||||||
|
if (expectedLaunchArguments != null) {
|
||||||
|
expect(expectedLaunchArguments, launchArguments);
|
||||||
|
}
|
||||||
|
debugStarted = debugSuccess;
|
||||||
|
|
||||||
|
if (completer != null) {
|
||||||
|
await completer!.future;
|
||||||
|
}
|
||||||
|
return debugSuccess;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<bool> exit({
|
||||||
|
bool force = false,
|
||||||
|
bool skipDelay = false,
|
||||||
|
}) async {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {}
|
||||||
|
|
||||||
|
class FakeShutDownHooks extends Fake implements ShutdownHooks {
|
||||||
|
List<ShutdownHook> hooks = <ShutdownHook>[];
|
||||||
|
@override
|
||||||
|
void addShutdownHook(ShutdownHook shutdownHook) {
|
||||||
|
hooks.add(shutdownHook);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
1033
packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart
Normal file
1033
packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart
Normal file
File diff suppressed because it is too large
Load Diff
@ -1306,5 +1306,75 @@ flutter:
|
|||||||
expectedBuildNumber: '1',
|
expectedBuildNumber: '1',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('CoreDevice', () {
|
||||||
|
testUsingContext('sets BUILD_DIR for core devices in debug mode', () async {
|
||||||
|
const BuildInfo buildInfo = BuildInfo.debug;
|
||||||
|
final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
|
||||||
|
await updateGeneratedXcodeProperties(
|
||||||
|
project: project,
|
||||||
|
buildInfo: buildInfo,
|
||||||
|
useMacOSConfig: true,
|
||||||
|
usingCoreDevice: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final File config = fs.file('path/to/project/macos/Flutter/ephemeral/Flutter-Generated.xcconfig');
|
||||||
|
expect(config.existsSync(), isTrue);
|
||||||
|
|
||||||
|
final String contents = config.readAsStringSync();
|
||||||
|
expect(contents, contains('\nBUILD_DIR=/build/ios\n'));
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
Artifacts: () => localIosArtifacts,
|
||||||
|
Platform: () => macOS,
|
||||||
|
FileSystem: () => fs,
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
XcodeProjectInterpreter: () => xcodeProjectInterpreter,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('does not set BUILD_DIR for core devices in release mode', () async {
|
||||||
|
const BuildInfo buildInfo = BuildInfo.release;
|
||||||
|
final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
|
||||||
|
await updateGeneratedXcodeProperties(
|
||||||
|
project: project,
|
||||||
|
buildInfo: buildInfo,
|
||||||
|
useMacOSConfig: true,
|
||||||
|
usingCoreDevice: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final File config = fs.file('path/to/project/macos/Flutter/ephemeral/Flutter-Generated.xcconfig');
|
||||||
|
expect(config.existsSync(), isTrue);
|
||||||
|
|
||||||
|
final String contents = config.readAsStringSync();
|
||||||
|
expect(contents.contains('\nBUILD_DIR'), isFalse);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
Artifacts: () => localIosArtifacts,
|
||||||
|
Platform: () => macOS,
|
||||||
|
FileSystem: () => fs,
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
XcodeProjectInterpreter: () => xcodeProjectInterpreter,
|
||||||
|
});
|
||||||
|
|
||||||
|
testUsingContext('does not set BUILD_DIR for non core devices', () async {
|
||||||
|
const BuildInfo buildInfo = BuildInfo.debug;
|
||||||
|
final FlutterProject project = FlutterProject.fromDirectoryTest(fs.directory('path/to/project'));
|
||||||
|
await updateGeneratedXcodeProperties(
|
||||||
|
project: project,
|
||||||
|
buildInfo: buildInfo,
|
||||||
|
useMacOSConfig: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
final File config = fs.file('path/to/project/macos/Flutter/ephemeral/Flutter-Generated.xcconfig');
|
||||||
|
expect(config.existsSync(), isTrue);
|
||||||
|
|
||||||
|
final String contents = config.readAsStringSync();
|
||||||
|
expect(contents.contains('\nBUILD_DIR'), isFalse);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
Artifacts: () => localIosArtifacts,
|
||||||
|
Platform: () => macOS,
|
||||||
|
FileSystem: () => fs,
|
||||||
|
ProcessManager: () => FakeProcessManager.any(),
|
||||||
|
XcodeProjectInterpreter: () => xcodeProjectInterpreter,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -4,16 +4,20 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:file/memory.dart';
|
||||||
import 'package:flutter_tools/src/artifacts.dart';
|
import 'package:flutter_tools/src/artifacts.dart';
|
||||||
import 'package:flutter_tools/src/base/io.dart' show ProcessException;
|
import 'package:flutter_tools/src/base/io.dart' show ProcessException;
|
||||||
import 'package:flutter_tools/src/base/logger.dart';
|
import 'package:flutter_tools/src/base/logger.dart';
|
||||||
import 'package:flutter_tools/src/base/platform.dart';
|
import 'package:flutter_tools/src/base/platform.dart';
|
||||||
|
import 'package:flutter_tools/src/base/user_messages.dart';
|
||||||
import 'package:flutter_tools/src/base/version.dart';
|
import 'package:flutter_tools/src/base/version.dart';
|
||||||
import 'package:flutter_tools/src/build_info.dart';
|
import 'package:flutter_tools/src/build_info.dart';
|
||||||
import 'package:flutter_tools/src/cache.dart';
|
import 'package:flutter_tools/src/cache.dart';
|
||||||
import 'package:flutter_tools/src/device.dart';
|
import 'package:flutter_tools/src/device.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/core_devices.dart';
|
||||||
import 'package:flutter_tools/src/ios/devices.dart';
|
import 'package:flutter_tools/src/ios/devices.dart';
|
||||||
import 'package:flutter_tools/src/ios/iproxy.dart';
|
import 'package:flutter_tools/src/ios/iproxy.dart';
|
||||||
|
import 'package:flutter_tools/src/ios/xcode_debug.dart';
|
||||||
import 'package:flutter_tools/src/ios/xcodeproj.dart';
|
import 'package:flutter_tools/src/ios/xcodeproj.dart';
|
||||||
import 'package:flutter_tools/src/macos/xcdevice.dart';
|
import 'package:flutter_tools/src/macos/xcdevice.dart';
|
||||||
import 'package:flutter_tools/src/macos/xcode.dart';
|
import 'package:flutter_tools/src/macos/xcode.dart';
|
||||||
@ -75,7 +79,7 @@ void main() {
|
|||||||
expect(fakeProcessManager, hasNoRemainingExpectations);
|
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('isSimctlInstalled is true when simctl list fails', () {
|
testWithoutContext('isSimctlInstalled is false when simctl list fails', () {
|
||||||
fakeProcessManager.addCommand(
|
fakeProcessManager.addCommand(
|
||||||
const FakeCommand(
|
const FakeCommand(
|
||||||
command: <String>[
|
command: <String>[
|
||||||
@ -97,6 +101,156 @@ void main() {
|
|||||||
expect(fakeProcessManager, hasNoRemainingExpectations);
|
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('isDevicectlInstalled', () {
|
||||||
|
testWithoutContext('is true when Xcode is 15+ and devicectl succeeds', () {
|
||||||
|
fakeProcessManager.addCommand(
|
||||||
|
const FakeCommand(
|
||||||
|
command: <String>[
|
||||||
|
'xcrun',
|
||||||
|
'devicectl',
|
||||||
|
'--version',
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
xcodeProjectInterpreter.version = Version(15, 0, 0);
|
||||||
|
final Xcode xcode = Xcode.test(
|
||||||
|
processManager: fakeProcessManager,
|
||||||
|
xcodeProjectInterpreter: xcodeProjectInterpreter,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(xcode.isDevicectlInstalled, isTrue);
|
||||||
|
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('is false when devicectl fails', () {
|
||||||
|
fakeProcessManager.addCommand(
|
||||||
|
const FakeCommand(
|
||||||
|
command: <String>[
|
||||||
|
'xcrun',
|
||||||
|
'devicectl',
|
||||||
|
'--version',
|
||||||
|
],
|
||||||
|
exitCode: 1,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
xcodeProjectInterpreter.version = Version(15, 0, 0);
|
||||||
|
final Xcode xcode = Xcode.test(
|
||||||
|
processManager: fakeProcessManager,
|
||||||
|
xcodeProjectInterpreter: xcodeProjectInterpreter,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(xcode.isDevicectlInstalled, isFalse);
|
||||||
|
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('is false when Xcode is less than 15', () {
|
||||||
|
xcodeProjectInterpreter.version = Version(14, 0, 0);
|
||||||
|
final Xcode xcode = Xcode.test(
|
||||||
|
processManager: fakeProcessManager,
|
||||||
|
xcodeProjectInterpreter: xcodeProjectInterpreter,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(xcode.isDevicectlInstalled, isFalse);
|
||||||
|
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('pathToXcodeApp', () {
|
||||||
|
late UserMessages userMessages;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
userMessages = UserMessages();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('parses correctly', () {
|
||||||
|
final Xcode xcode = Xcode.test(
|
||||||
|
processManager: fakeProcessManager,
|
||||||
|
xcodeProjectInterpreter: xcodeProjectInterpreter,
|
||||||
|
);
|
||||||
|
|
||||||
|
fakeProcessManager.addCommand(const FakeCommand(
|
||||||
|
command: <String>['/usr/bin/xcode-select', '--print-path'],
|
||||||
|
stdout: '/Applications/Xcode.app/Contents/Developer',
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(xcode.xcodeAppPath, '/Applications/Xcode.app');
|
||||||
|
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('throws error if not found', () {
|
||||||
|
final Xcode xcode = Xcode.test(
|
||||||
|
processManager: FakeProcessManager.any(),
|
||||||
|
xcodeProjectInterpreter: xcodeProjectInterpreter,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => xcode.xcodeAppPath,
|
||||||
|
throwsToolExit(message: userMessages.xcodeMissing),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('throws error with unexpected outcome', () {
|
||||||
|
final Xcode xcode = Xcode.test(
|
||||||
|
processManager: fakeProcessManager,
|
||||||
|
xcodeProjectInterpreter: xcodeProjectInterpreter,
|
||||||
|
);
|
||||||
|
|
||||||
|
fakeProcessManager.addCommand(const FakeCommand(
|
||||||
|
command: <String>[
|
||||||
|
'/usr/bin/xcode-select',
|
||||||
|
'--print-path',
|
||||||
|
],
|
||||||
|
stdout: '/Library/Developer/CommandLineTools',
|
||||||
|
));
|
||||||
|
|
||||||
|
expect(
|
||||||
|
() => xcode.xcodeAppPath,
|
||||||
|
throwsToolExit(message: userMessages.xcodeMissing),
|
||||||
|
);
|
||||||
|
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('pathToXcodeAutomationScript', () {
|
||||||
|
const String flutterRoot = '/path/to/flutter';
|
||||||
|
|
||||||
|
late MemoryFileSystem fileSystem;
|
||||||
|
|
||||||
|
setUp(() {
|
||||||
|
fileSystem = MemoryFileSystem.test();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('returns path when file is found', () {
|
||||||
|
final Xcode xcode = Xcode.test(
|
||||||
|
processManager: fakeProcessManager,
|
||||||
|
xcodeProjectInterpreter: xcodeProjectInterpreter,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
flutterRoot: flutterRoot,
|
||||||
|
);
|
||||||
|
|
||||||
|
fileSystem.file('$flutterRoot/packages/flutter_tools/bin/xcode_debug.js').createSync(recursive: true);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
xcode.xcodeAutomationScriptPath,
|
||||||
|
'$flutterRoot/packages/flutter_tools/bin/xcode_debug.js',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('throws error when not found', () {
|
||||||
|
final Xcode xcode = Xcode.test(
|
||||||
|
processManager: fakeProcessManager,
|
||||||
|
xcodeProjectInterpreter: xcodeProjectInterpreter,
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
flutterRoot: flutterRoot,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() =>
|
||||||
|
xcode.xcodeAutomationScriptPath,
|
||||||
|
throwsToolExit()
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('macOS', () {
|
group('macOS', () {
|
||||||
late Xcode xcode;
|
late Xcode xcode;
|
||||||
late BufferLogger logger;
|
late BufferLogger logger;
|
||||||
@ -339,6 +493,7 @@ void main() {
|
|||||||
group('xcdevice not installed', () {
|
group('xcdevice not installed', () {
|
||||||
late XCDevice xcdevice;
|
late XCDevice xcdevice;
|
||||||
late Xcode xcode;
|
late Xcode xcode;
|
||||||
|
late MemoryFileSystem fileSystem;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
xcode = Xcode.test(
|
xcode = Xcode.test(
|
||||||
@ -348,6 +503,7 @@ void main() {
|
|||||||
version: null, // Not installed.
|
version: null, // Not installed.
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
fileSystem = MemoryFileSystem.test();
|
||||||
xcdevice = XCDevice(
|
xcdevice = XCDevice(
|
||||||
processManager: fakeProcessManager,
|
processManager: fakeProcessManager,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
@ -356,6 +512,9 @@ void main() {
|
|||||||
artifacts: Artifacts.test(),
|
artifacts: Artifacts.test(),
|
||||||
cache: Cache.test(processManager: FakeProcessManager.any()),
|
cache: Cache.test(processManager: FakeProcessManager.any()),
|
||||||
iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
|
iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
coreDeviceControl: FakeIOSCoreDeviceControl(),
|
||||||
|
xcodeDebug: FakeXcodeDebug(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -373,9 +532,13 @@ void main() {
|
|||||||
group('xcdevice', () {
|
group('xcdevice', () {
|
||||||
late XCDevice xcdevice;
|
late XCDevice xcdevice;
|
||||||
late Xcode xcode;
|
late Xcode xcode;
|
||||||
|
late MemoryFileSystem fileSystem;
|
||||||
|
late FakeIOSCoreDeviceControl coreDeviceControl;
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
xcode = Xcode.test(processManager: FakeProcessManager.any());
|
xcode = Xcode.test(processManager: FakeProcessManager.any());
|
||||||
|
fileSystem = MemoryFileSystem.test();
|
||||||
|
coreDeviceControl = FakeIOSCoreDeviceControl();
|
||||||
xcdevice = XCDevice(
|
xcdevice = XCDevice(
|
||||||
processManager: fakeProcessManager,
|
processManager: fakeProcessManager,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
@ -384,6 +547,9 @@ void main() {
|
|||||||
artifacts: Artifacts.test(),
|
artifacts: Artifacts.test(),
|
||||||
cache: Cache.test(processManager: FakeProcessManager.any()),
|
cache: Cache.test(processManager: FakeProcessManager.any()),
|
||||||
iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
|
iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
|
||||||
|
fileSystem: fileSystem,
|
||||||
|
coreDeviceControl: coreDeviceControl,
|
||||||
|
xcodeDebug: FakeXcodeDebug(),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -1117,6 +1283,176 @@ void main() {
|
|||||||
}, overrides: <Type, Generator>{
|
}, overrides: <Type, Generator>{
|
||||||
Platform: () => macPlatform,
|
Platform: () => macPlatform,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('with CoreDevices', () {
|
||||||
|
testUsingContext('returns devices with corresponding CoreDevices', () async {
|
||||||
|
const String devicesOutput = '''
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"simulator" : true,
|
||||||
|
"operatingSystemVersion" : "13.3 (17K446)",
|
||||||
|
"available" : true,
|
||||||
|
"platform" : "com.apple.platform.appletvsimulator",
|
||||||
|
"modelCode" : "AppleTV5,3",
|
||||||
|
"identifier" : "CBB5E1ED-2172-446E-B4E7-F2B5823DBBA6",
|
||||||
|
"architecture" : "x86_64",
|
||||||
|
"modelName" : "Apple TV",
|
||||||
|
"name" : "Apple TV"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"simulator" : false,
|
||||||
|
"operatingSystemVersion" : "13.3 (17C54)",
|
||||||
|
"interface" : "usb",
|
||||||
|
"available" : true,
|
||||||
|
"platform" : "com.apple.platform.iphoneos",
|
||||||
|
"modelCode" : "iPhone8,1",
|
||||||
|
"identifier" : "00008027-00192736010F802E",
|
||||||
|
"architecture" : "arm64",
|
||||||
|
"modelName" : "iPhone 6s",
|
||||||
|
"name" : "An iPhone (Space Gray)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"simulator" : false,
|
||||||
|
"operatingSystemVersion" : "10.1 (14C54)",
|
||||||
|
"interface" : "usb",
|
||||||
|
"available" : true,
|
||||||
|
"platform" : "com.apple.platform.iphoneos",
|
||||||
|
"modelCode" : "iPad11,4",
|
||||||
|
"identifier" : "98206e7a4afd4aedaff06e687594e089dede3c44",
|
||||||
|
"architecture" : "armv7",
|
||||||
|
"modelName" : "iPad Air 3rd Gen",
|
||||||
|
"name" : "iPad 1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"simulator" : false,
|
||||||
|
"operatingSystemVersion" : "10.1 (14C54)",
|
||||||
|
"interface" : "network",
|
||||||
|
"available" : true,
|
||||||
|
"platform" : "com.apple.platform.iphoneos",
|
||||||
|
"modelCode" : "iPad11,4",
|
||||||
|
"identifier" : "234234234234234234345445687594e089dede3c44",
|
||||||
|
"architecture" : "arm64",
|
||||||
|
"modelName" : "iPad Air 3rd Gen",
|
||||||
|
"name" : "A networked iPad"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"simulator" : false,
|
||||||
|
"operatingSystemVersion" : "10.1 (14C54)",
|
||||||
|
"interface" : "usb",
|
||||||
|
"available" : true,
|
||||||
|
"platform" : "com.apple.platform.iphoneos",
|
||||||
|
"modelCode" : "iPad11,4",
|
||||||
|
"identifier" : "f577a7903cc54959be2e34bc4f7f80b7009efcf4",
|
||||||
|
"architecture" : "BOGUS",
|
||||||
|
"modelName" : "iPad Air 3rd Gen",
|
||||||
|
"name" : "iPad 2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"simulator" : true,
|
||||||
|
"operatingSystemVersion" : "6.1.1 (17S445)",
|
||||||
|
"available" : true,
|
||||||
|
"platform" : "com.apple.platform.watchsimulator",
|
||||||
|
"modelCode" : "Watch5,4",
|
||||||
|
"identifier" : "2D74FB11-88A0-44D0-B81E-C0C142B1C94A",
|
||||||
|
"architecture" : "i386",
|
||||||
|
"modelName" : "Apple Watch Series 5 - 44mm",
|
||||||
|
"name" : "Apple Watch Series 5 - 44mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"simulator" : false,
|
||||||
|
"operatingSystemVersion" : "13.3 (17C54)",
|
||||||
|
"interface" : "usb",
|
||||||
|
"available" : false,
|
||||||
|
"platform" : "com.apple.platform.iphoneos",
|
||||||
|
"modelCode" : "iPhone8,1",
|
||||||
|
"identifier" : "c4ca6f7a53027d1b7e4972e28478e7a28e2faee2",
|
||||||
|
"architecture" : "arm64",
|
||||||
|
"modelName" : "iPhone 6s",
|
||||||
|
"name" : "iPhone",
|
||||||
|
"error" : {
|
||||||
|
"code" : -9,
|
||||||
|
"failureReason" : "",
|
||||||
|
"description" : "iPhone is not paired with your computer.",
|
||||||
|
"domain" : "com.apple.platform.iphoneos"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
''';
|
||||||
|
coreDeviceControl.devices.addAll(<FakeIOSCoreDevice>[
|
||||||
|
FakeIOSCoreDevice(
|
||||||
|
udid: '00008027-00192736010F802E',
|
||||||
|
connectionInterface: DeviceConnectionInterface.wireless,
|
||||||
|
developerModeStatus: 'enabled',
|
||||||
|
),
|
||||||
|
FakeIOSCoreDevice(
|
||||||
|
connectionInterface: DeviceConnectionInterface.wireless,
|
||||||
|
developerModeStatus: 'enabled',
|
||||||
|
),
|
||||||
|
FakeIOSCoreDevice(
|
||||||
|
udid: '234234234234234234345445687594e089dede3c44',
|
||||||
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
|
),
|
||||||
|
FakeIOSCoreDevice(
|
||||||
|
udid: 'f577a7903cc54959be2e34bc4f7f80b7009efcf4',
|
||||||
|
connectionInterface: DeviceConnectionInterface.attached,
|
||||||
|
developerModeStatus: 'disabled',
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
fakeProcessManager.addCommand(const FakeCommand(
|
||||||
|
command: <String>['xcrun', 'xcdevice', 'list', '--timeout', '2'],
|
||||||
|
stdout: devicesOutput,
|
||||||
|
));
|
||||||
|
|
||||||
|
final List<IOSDevice> devices = await xcdevice.getAvailableIOSDevices();
|
||||||
|
expect(devices, hasLength(5));
|
||||||
|
expect(devices[0].id, '00008027-00192736010F802E');
|
||||||
|
expect(devices[0].name, 'An iPhone (Space Gray)');
|
||||||
|
expect(await devices[0].sdkNameAndVersion, 'iOS 13.3 17C54');
|
||||||
|
expect(devices[0].cpuArchitecture, DarwinArch.arm64);
|
||||||
|
expect(devices[0].connectionInterface, DeviceConnectionInterface.wireless);
|
||||||
|
expect(devices[0].isConnected, true);
|
||||||
|
expect(devices[0].devModeEnabled, true);
|
||||||
|
|
||||||
|
expect(devices[1].id, '98206e7a4afd4aedaff06e687594e089dede3c44');
|
||||||
|
expect(devices[1].name, 'iPad 1');
|
||||||
|
expect(await devices[1].sdkNameAndVersion, 'iOS 10.1 14C54');
|
||||||
|
expect(devices[1].cpuArchitecture, DarwinArch.armv7);
|
||||||
|
expect(devices[1].connectionInterface, DeviceConnectionInterface.attached);
|
||||||
|
expect(devices[1].isConnected, true);
|
||||||
|
expect(devices[1].devModeEnabled, true);
|
||||||
|
|
||||||
|
expect(devices[2].id, '234234234234234234345445687594e089dede3c44');
|
||||||
|
expect(devices[2].name, 'A networked iPad');
|
||||||
|
expect(await devices[2].sdkNameAndVersion, 'iOS 10.1 14C54');
|
||||||
|
expect(devices[2].cpuArchitecture, DarwinArch.arm64); // Defaults to arm64 for unknown architecture.
|
||||||
|
expect(devices[2].connectionInterface, DeviceConnectionInterface.attached);
|
||||||
|
expect(devices[2].isConnected, true);
|
||||||
|
expect(devices[2].devModeEnabled, false);
|
||||||
|
|
||||||
|
expect(devices[3].id, 'f577a7903cc54959be2e34bc4f7f80b7009efcf4');
|
||||||
|
expect(devices[3].name, 'iPad 2');
|
||||||
|
expect(await devices[3].sdkNameAndVersion, 'iOS 10.1 14C54');
|
||||||
|
expect(devices[3].cpuArchitecture, DarwinArch.arm64); // Defaults to arm64 for unknown architecture.
|
||||||
|
expect(devices[3].connectionInterface, DeviceConnectionInterface.attached);
|
||||||
|
expect(devices[3].isConnected, true);
|
||||||
|
expect(devices[3].devModeEnabled, false);
|
||||||
|
|
||||||
|
expect(devices[4].id, 'c4ca6f7a53027d1b7e4972e28478e7a28e2faee2');
|
||||||
|
expect(devices[4].name, 'iPhone');
|
||||||
|
expect(await devices[4].sdkNameAndVersion, 'iOS 13.3 17C54');
|
||||||
|
expect(devices[4].cpuArchitecture, DarwinArch.arm64);
|
||||||
|
expect(devices[4].connectionInterface, DeviceConnectionInterface.attached);
|
||||||
|
expect(devices[4].isConnected, false);
|
||||||
|
expect(devices[4].devModeEnabled, true);
|
||||||
|
|
||||||
|
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||||
|
}, overrides: <Type, Generator>{
|
||||||
|
Platform: () => macPlatform,
|
||||||
|
Artifacts: () => Artifacts.test(),
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
group('diagnostics', () {
|
group('diagnostics', () {
|
||||||
@ -1312,3 +1648,41 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete
|
|||||||
@override
|
@override
|
||||||
List<String> xcrunCommand() => <String>['xcrun'];
|
List<String> xcrunCommand() => <String>['xcrun'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FakeXcodeDebug extends Fake implements XcodeDebug {}
|
||||||
|
|
||||||
|
class FakeIOSCoreDeviceControl extends Fake implements IOSCoreDeviceControl {
|
||||||
|
|
||||||
|
List<FakeIOSCoreDevice> devices = <FakeIOSCoreDevice>[];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<IOSCoreDevice>> getCoreDevices({Duration timeout = Duration.zero}) async {
|
||||||
|
return devices;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeIOSCoreDevice extends Fake implements IOSCoreDevice {
|
||||||
|
FakeIOSCoreDevice({
|
||||||
|
this.udid,
|
||||||
|
this.connectionInterface,
|
||||||
|
this.developerModeStatus,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? developerModeStatus;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String? udid;
|
||||||
|
|
||||||
|
@override
|
||||||
|
final DeviceConnectionInterface? connectionInterface;
|
||||||
|
|
||||||
|
@override
|
||||||
|
IOSCoreDeviceProperties? get deviceProperties => FakeIOSCoreDeviceProperties(developerModeStatus: developerModeStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FakeIOSCoreDeviceProperties extends Fake implements IOSCoreDeviceProperties {
|
||||||
|
FakeIOSCoreDeviceProperties({required this.developerModeStatus});
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String? developerModeStatus;
|
||||||
|
}
|
||||||
|
@ -478,6 +478,18 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
group('for launch', () {
|
group('for launch', () {
|
||||||
|
testWithoutContext('Ensure either port or device name are provided', () async {
|
||||||
|
final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
|
||||||
|
|
||||||
|
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
|
||||||
|
mdnsClient: client,
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
flutterUsage: TestUsage(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(() async => portDiscovery.queryForLaunch(applicationId: 'app-id'), throwsAssertionError);
|
||||||
|
});
|
||||||
|
|
||||||
testWithoutContext('No ports available', () async {
|
testWithoutContext('No ports available', () async {
|
||||||
final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
|
final MDnsClient client = FakeMDnsClient(<PtrResourceRecord>[], <String, List<SrvResourceRecord>>{});
|
||||||
|
|
||||||
@ -666,6 +678,93 @@ void main() {
|
|||||||
message:'Did not find a Dart VM Service advertised for srv-bar on port 321.'),
|
message:'Did not find a Dart VM Service advertised for srv-bar on port 321.'),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWithoutContext('Matches on application id and device name', () async {
|
||||||
|
final MDnsClient client = FakeMDnsClient(
|
||||||
|
<PtrResourceRecord>[
|
||||||
|
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
|
||||||
|
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
|
||||||
|
PtrResourceRecord('baz', future, domainName: 'srv-boo'),
|
||||||
|
],
|
||||||
|
<String, List<SrvResourceRecord>>{
|
||||||
|
'srv-bar': <SrvResourceRecord>[
|
||||||
|
SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'My-Phone.local'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final FakeIOSDevice device = FakeIOSDevice(
|
||||||
|
name: 'My Phone',
|
||||||
|
);
|
||||||
|
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
|
||||||
|
mdnsClient: client,
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
flutterUsage: TestUsage(),
|
||||||
|
);
|
||||||
|
|
||||||
|
final Uri? uri = await portDiscovery.getVMServiceUriForLaunch(
|
||||||
|
'srv-bar',
|
||||||
|
device,
|
||||||
|
);
|
||||||
|
expect(uri.toString(), 'http://127.0.0.1:123/');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('Throw error if unable to find VM Service with app id and device name', () async {
|
||||||
|
final MDnsClient client = FakeMDnsClient(
|
||||||
|
<PtrResourceRecord>[
|
||||||
|
PtrResourceRecord('foo', future, domainName: 'srv-foo'),
|
||||||
|
PtrResourceRecord('bar', future, domainName: 'srv-bar'),
|
||||||
|
PtrResourceRecord('baz', future, domainName: 'srv-boo'),
|
||||||
|
],
|
||||||
|
<String, List<SrvResourceRecord>>{
|
||||||
|
'srv-foo': <SrvResourceRecord>[
|
||||||
|
SrvResourceRecord('srv-foo', future, port: 123, weight: 1, priority: 1, target: 'target-foo'),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final FakeIOSDevice device = FakeIOSDevice(
|
||||||
|
name: 'My Phone',
|
||||||
|
);
|
||||||
|
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
|
||||||
|
mdnsClient: client,
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
flutterUsage: TestUsage(),
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
portDiscovery.getVMServiceUriForLaunch(
|
||||||
|
'srv-bar',
|
||||||
|
device,
|
||||||
|
),
|
||||||
|
throwsToolExit(
|
||||||
|
message:'Did not find a Dart VM Service advertised for srv-bar'),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('deviceNameMatchesTargetName', () {
|
||||||
|
testWithoutContext('compares case insensitive and without spaces, hypthens, .local', () {
|
||||||
|
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
|
||||||
|
mdnsClient: FakeMDnsClient(
|
||||||
|
<PtrResourceRecord>[],
|
||||||
|
<String, List<SrvResourceRecord>>{},
|
||||||
|
),
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
flutterUsage: TestUsage(),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(portDiscovery.deviceNameMatchesTargetName('My phone', 'My-Phone.local'), isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWithoutContext('includes numbers in comparison', () {
|
||||||
|
final MDnsVmServiceDiscovery portDiscovery = MDnsVmServiceDiscovery(
|
||||||
|
mdnsClient: FakeMDnsClient(
|
||||||
|
<PtrResourceRecord>[],
|
||||||
|
<String, List<SrvResourceRecord>>{},
|
||||||
|
),
|
||||||
|
logger: BufferLogger.test(),
|
||||||
|
flutterUsage: TestUsage(),
|
||||||
|
);
|
||||||
|
expect(portDiscovery.deviceNameMatchesTargetName('My phone', 'My-Phone-2.local'), isFalse);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
testWithoutContext('Find firstMatchingVmService with many available and no application id', () async {
|
testWithoutContext('Find firstMatchingVmService with many available and no application id', () async {
|
||||||
@ -895,6 +994,11 @@ class FakeMDnsClient extends Fake implements MDnsClient {
|
|||||||
// Until we fix that, we have to also ignore related lints here.
|
// Until we fix that, we have to also ignore related lints here.
|
||||||
// ignore: avoid_implementing_value_types
|
// ignore: avoid_implementing_value_types
|
||||||
class FakeIOSDevice extends Fake implements IOSDevice {
|
class FakeIOSDevice extends Fake implements IOSDevice {
|
||||||
|
FakeIOSDevice({this.name = 'iPhone'});
|
||||||
|
|
||||||
|
@override
|
||||||
|
final String name;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
|
Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user