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"]
|
||||
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
|
||||
recipe: devicelab/devicelab_drone
|
||||
presubmit: false
|
||||
@ -3752,6 +3762,16 @@ targets:
|
||||
["devicelab", "ios", "mac"]
|
||||
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
|
||||
recipe: devicelab/devicelab_drone
|
||||
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_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_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_view_ios__start_up.dart @zanderso @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/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_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_keyboard_resize.dart @cyanglaz @flutter/engine
|
||||
/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;
|
||||
}
|
||||
|
||||
TaskFunction createEndToEndDriverTest() {
|
||||
TaskFunction createEndToEndDriverTest({Map<String, String>? environment}) {
|
||||
return DriverTest(
|
||||
'${flutterDirectory.path}/dev/integration_tests/ui',
|
||||
'lib/driver.dart',
|
||||
environment: environment,
|
||||
).call;
|
||||
}
|
||||
|
||||
@ -173,6 +174,7 @@ class DriverTest {
|
||||
this.testTarget, {
|
||||
this.extraOptions = const <String>[],
|
||||
this.deviceIdOverride,
|
||||
this.environment,
|
||||
}
|
||||
);
|
||||
|
||||
@ -180,6 +182,7 @@ class DriverTest {
|
||||
final String testTarget;
|
||||
final List<String> extraOptions;
|
||||
final String? deviceIdOverride;
|
||||
final Map<String, String>? environment;
|
||||
|
||||
Future<TaskResult> call() {
|
||||
return inDirectory<TaskResult>(testDirectory, () async {
|
||||
@ -202,7 +205,7 @@ class DriverTest {
|
||||
deviceId,
|
||||
...extraOptions,
|
||||
];
|
||||
await flutter('drive', options: options);
|
||||
await flutter('drive', options: options, environment: environment);
|
||||
|
||||
return TaskResult.success(null);
|
||||
});
|
||||
|
@ -233,10 +233,11 @@ TaskFunction createOpenPayScrollPerfTest({bool measureCpuGpu = true}) {
|
||||
).run;
|
||||
}
|
||||
|
||||
TaskFunction createFlutterGalleryStartupTest({String target = 'lib/main.dart'}) {
|
||||
TaskFunction createFlutterGalleryStartupTest({String target = 'lib/main.dart', Map<String, String>? runEnvironment}) {
|
||||
return StartupTest(
|
||||
'${flutterDirectory.path}/dev/integration_tests/flutter_gallery',
|
||||
target: target,
|
||||
runEnvironment: runEnvironment,
|
||||
).run;
|
||||
}
|
||||
|
||||
@ -768,11 +769,17 @@ Future<void> _resetManifest(String testDirectory) async {
|
||||
|
||||
/// Measure application startup performance.
|
||||
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 bool reportMetrics;
|
||||
final String target;
|
||||
final Map<String, String>? runEnvironment;
|
||||
|
||||
Future<TaskResult> run() async {
|
||||
return inDirectory<TaskResult>(testDirectory, () async {
|
||||
@ -855,21 +862,26 @@ class StartupTest {
|
||||
'screenshot_startup_${DateTime.now().toLocal().toIso8601String()}.png',
|
||||
);
|
||||
});
|
||||
final int result = await flutter('run', options: <String>[
|
||||
'--no-android-gradle-daemon',
|
||||
'--no-publish-port',
|
||||
'--verbose',
|
||||
'--profile',
|
||||
'--trace-startup',
|
||||
// TODO(vashworth): Remove once done debugging https://github.com/flutter/flutter/issues/129836
|
||||
if (device is IosDevice)
|
||||
'--verbose-system-logs',
|
||||
'--target=$target',
|
||||
'-d',
|
||||
device.deviceId,
|
||||
if (applicationBinaryPath != null)
|
||||
'--use-application-binary=$applicationBinaryPath',
|
||||
], canFail: true);
|
||||
final int result = await flutter(
|
||||
'run',
|
||||
options: <String>[
|
||||
'--no-android-gradle-daemon',
|
||||
'--no-publish-port',
|
||||
'--verbose',
|
||||
'--profile',
|
||||
'--trace-startup',
|
||||
// TODO(vashworth): Remove once done debugging https://github.com/flutter/flutter/issues/129836
|
||||
if (device is IosDevice)
|
||||
'--verbose-system-logs',
|
||||
'--target=$target',
|
||||
'-d',
|
||||
device.deviceId,
|
||||
if (applicationBinaryPath != null)
|
||||
'--use-application-binary=$applicationBinaryPath',
|
||||
],
|
||||
environment: runEnvironment,
|
||||
canFail: true,
|
||||
);
|
||||
timer.cancel();
|
||||
if (result == 0) {
|
||||
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,
|
||||
fileSystem: globals.fs,
|
||||
xcodeProjectInterpreter: globals.xcodeProjectInterpreter!,
|
||||
userMessages: globals.userMessages,
|
||||
),
|
||||
XCDevice: () => XCDevice(
|
||||
processManager: globals.processManager,
|
||||
@ -375,6 +376,7 @@ Future<T> runInContext<T>(
|
||||
processManager: globals.processManager,
|
||||
dyLdLibEntry: globals.cache.dyLdLibEntry,
|
||||
),
|
||||
fileSystem: globals.fs,
|
||||
),
|
||||
XcodeProjectInterpreter: () => XcodeProjectInterpreter(
|
||||
logger: globals.logger,
|
||||
|
@ -1159,6 +1159,7 @@ class DebuggingOptions {
|
||||
Map<String, Object?> platformArgs, {
|
||||
bool ipv6 = false,
|
||||
DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached,
|
||||
bool isCoreDevice = false,
|
||||
}) {
|
||||
final String dartVmFlags = computeDartVmFlags(this);
|
||||
return <String>[
|
||||
@ -1172,7 +1173,10 @@ class DebuggingOptions {
|
||||
if (environmentType == EnvironmentType.simulator && dartVmFlags.isNotEmpty)
|
||||
'--dart-flags=$dartVmFlags',
|
||||
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',
|
||||
'--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/os.dart';
|
||||
import '../base/platform.dart';
|
||||
import '../base/process.dart';
|
||||
import '../base/utils.dart';
|
||||
import '../base/version.dart';
|
||||
import '../build_info.dart';
|
||||
@ -28,10 +29,13 @@ import '../project.dart';
|
||||
import '../protocol_discovery.dart';
|
||||
import '../vmservice.dart';
|
||||
import 'application_package.dart';
|
||||
import 'core_devices.dart';
|
||||
import 'ios_deploy.dart';
|
||||
import 'ios_workflow.dart';
|
||||
import 'iproxy.dart';
|
||||
import 'mac.dart';
|
||||
import 'xcode_debug.dart';
|
||||
import 'xcodeproj.dart';
|
||||
|
||||
class IOSDevices extends PollingDeviceDiscovery {
|
||||
IOSDevices({
|
||||
@ -263,16 +267,21 @@ class IOSDevice extends Device {
|
||||
required this.connectionInterface,
|
||||
required this.isConnected,
|
||||
required this.devModeEnabled,
|
||||
required this.isCoreDevice,
|
||||
String? sdkVersion,
|
||||
required Platform platform,
|
||||
required IOSDeploy iosDeploy,
|
||||
required IMobileDevice iMobileDevice,
|
||||
required IOSCoreDeviceControl coreDeviceControl,
|
||||
required XcodeDebug xcodeDebug,
|
||||
required IProxy iProxy,
|
||||
required Logger logger,
|
||||
})
|
||||
: _sdkVersion = sdkVersion,
|
||||
_iosDeploy = iosDeploy,
|
||||
_iMobileDevice = iMobileDevice,
|
||||
_coreDeviceControl = coreDeviceControl,
|
||||
_xcodeDebug = xcodeDebug,
|
||||
_iproxy = iProxy,
|
||||
_fileSystem = fileSystem,
|
||||
_logger = logger,
|
||||
@ -294,6 +303,8 @@ class IOSDevice extends Device {
|
||||
final Logger _logger;
|
||||
final Platform _platform;
|
||||
final IMobileDevice _iMobileDevice;
|
||||
final IOSCoreDeviceControl _coreDeviceControl;
|
||||
final XcodeDebug _xcodeDebug;
|
||||
final IProxy _iproxy;
|
||||
|
||||
Version? get sdkVersion {
|
||||
@ -324,6 +335,10 @@ class IOSDevice extends Device {
|
||||
@override
|
||||
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>{};
|
||||
|
||||
DevicePortForwarder? _portForwarder;
|
||||
@ -349,10 +364,17 @@ class IOSDevice extends Device {
|
||||
}) async {
|
||||
bool result;
|
||||
try {
|
||||
result = await _iosDeploy.isAppInstalled(
|
||||
bundleId: app.id,
|
||||
deviceId: id,
|
||||
);
|
||||
if (isCoreDevice) {
|
||||
result = await _coreDeviceControl.isAppInstalled(
|
||||
bundleId: app.id,
|
||||
deviceId: id,
|
||||
);
|
||||
} else {
|
||||
result = await _iosDeploy.isAppInstalled(
|
||||
bundleId: app.id,
|
||||
deviceId: id,
|
||||
);
|
||||
}
|
||||
} on ProcessException catch (e) {
|
||||
_logger.printError(e.message);
|
||||
return false;
|
||||
@ -376,13 +398,20 @@ class IOSDevice extends Device {
|
||||
|
||||
int installationResult;
|
||||
try {
|
||||
installationResult = await _iosDeploy.installApp(
|
||||
deviceId: id,
|
||||
bundlePath: bundle.path,
|
||||
appDeltaDirectory: app.appDeltaDirectory,
|
||||
launchArguments: <String>[],
|
||||
interfaceType: connectionInterface,
|
||||
);
|
||||
if (isCoreDevice) {
|
||||
installationResult = await _coreDeviceControl.installApp(
|
||||
deviceId: id,
|
||||
bundlePath: bundle.path,
|
||||
) ? 0 : 1;
|
||||
} else {
|
||||
installationResult = await _iosDeploy.installApp(
|
||||
deviceId: id,
|
||||
bundlePath: bundle.path,
|
||||
appDeltaDirectory: app.appDeltaDirectory,
|
||||
launchArguments: <String>[],
|
||||
interfaceType: connectionInterface,
|
||||
);
|
||||
}
|
||||
} on ProcessException catch (e) {
|
||||
_logger.printError(e.message);
|
||||
return false;
|
||||
@ -404,10 +433,17 @@ class IOSDevice extends Device {
|
||||
}) async {
|
||||
int uninstallationResult;
|
||||
try {
|
||||
uninstallationResult = await _iosDeploy.uninstallApp(
|
||||
deviceId: id,
|
||||
bundleId: app.id,
|
||||
);
|
||||
if (isCoreDevice) {
|
||||
uninstallationResult = await _coreDeviceControl.uninstallApp(
|
||||
deviceId: id,
|
||||
bundleId: app.id,
|
||||
) ? 0 : 1;
|
||||
} else {
|
||||
uninstallationResult = await _iosDeploy.uninstallApp(
|
||||
deviceId: id,
|
||||
bundleId: app.id,
|
||||
);
|
||||
}
|
||||
} on ProcessException catch (e) {
|
||||
_logger.printError(e.message);
|
||||
return false;
|
||||
@ -434,6 +470,7 @@ class IOSDevice extends Device {
|
||||
bool ipv6 = false,
|
||||
String? userIdentifier,
|
||||
@visibleForTesting Duration? discoveryTimeout,
|
||||
@visibleForTesting ShutdownHooks? shutdownHooks,
|
||||
}) async {
|
||||
String? packageId;
|
||||
if (isWirelesslyConnected &&
|
||||
@ -441,6 +478,18 @@ class IOSDevice extends Device {
|
||||
debuggingOptions.disablePortPublication) {
|
||||
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) {
|
||||
_logger.printTrace('Building ${package.name} for $id');
|
||||
|
||||
@ -451,6 +500,7 @@ class IOSDevice extends Device {
|
||||
targetOverride: mainPath,
|
||||
activeArch: cpuArchitecture,
|
||||
deviceID: id,
|
||||
isCoreDevice: isCoreDevice || forceXcodeDebugWorkflow,
|
||||
);
|
||||
if (!buildResult.success) {
|
||||
_logger.printError('Could not build the precompiled application for the device.');
|
||||
@ -477,6 +527,7 @@ class IOSDevice extends Device {
|
||||
platformArgs,
|
||||
ipv6: ipv6,
|
||||
interfaceType: connectionInterface,
|
||||
isCoreDevice: isCoreDevice,
|
||||
);
|
||||
Status startAppStatus = _logger.startProgress(
|
||||
'Installing and launching...',
|
||||
@ -516,7 +567,16 @@ class IOSDevice extends Device {
|
||||
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(
|
||||
deviceId: id,
|
||||
bundlePath: bundle.path,
|
||||
@ -543,10 +603,26 @@ class IOSDevice extends Device {
|
||||
|
||||
_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), () {
|
||||
_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
|
||||
// user to allow local network permissions.
|
||||
if (isWirelesslyConnected) {
|
||||
@ -564,37 +640,71 @@ class IOSDevice extends Device {
|
||||
|
||||
Uri? localUri;
|
||||
if (isWirelesslyConnected) {
|
||||
// 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();
|
||||
}
|
||||
|
||||
// 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
|
||||
// should only show this message if they have not already approved the permissions.
|
||||
// MDnsVmServiceDiscovery usually takes less than 5 seconds to find it.
|
||||
final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () {
|
||||
startAppStatus.stop();
|
||||
startAppStatus = _logger.startProgress(
|
||||
'Waiting for approval of local network permissions...',
|
||||
// When using a CoreDevice, device logs are unavailable and therefore
|
||||
// cannot be used to get the Dart VM url. Instead, get the Dart VM
|
||||
// Service by finding services matching the app bundle id and the
|
||||
// device name.
|
||||
//
|
||||
// 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.
|
||||
//
|
||||
// Then in both cases, get the device IP from the Dart VM Service to
|
||||
// construct the Dart VM url using the device IP as the host.
|
||||
if (isCoreDevice) {
|
||||
localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
|
||||
packageId,
|
||||
this,
|
||||
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.
|
||||
localUri = await MDnsVmServiceDiscovery.instance!.getVMServiceUriForLaunch(
|
||||
packageId,
|
||||
this,
|
||||
usesIpv6: ipv6,
|
||||
deviceVmservicePort: serviceURL.port,
|
||||
useDeviceIPAsHost: true,
|
||||
);
|
||||
// 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
|
||||
// should only show this message if they have not already approved the permissions.
|
||||
// MDnsVmServiceDiscovery usually takes less than 5 seconds to find it.
|
||||
final Timer mDNSLookupTimer = Timer(const Duration(seconds: 5), () {
|
||||
startAppStatus.stop();
|
||||
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 {
|
||||
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();
|
||||
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
|
||||
Future<bool> stopApp(
|
||||
ApplicationPackage? app, {
|
||||
@ -623,6 +837,9 @@ class IOSDevice extends Device {
|
||||
if (deployDebugger != null && deployDebugger.debuggerAttached) {
|
||||
return deployDebugger.exit();
|
||||
}
|
||||
if (_xcodeDebug.debugStarted) {
|
||||
return _xcodeDebug.exit();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -669,7 +886,14 @@ class IOSDevice extends Device {
|
||||
void clearLogs() { }
|
||||
|
||||
@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
|
||||
Future<void> takeScreenshot(File outputFile) async {
|
||||
@ -757,14 +981,18 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
||||
this._majorSdkVersion,
|
||||
this._deviceId,
|
||||
this.name,
|
||||
this._isWirelesslyConnected,
|
||||
this._isCoreDevice,
|
||||
String appName,
|
||||
bool usingCISystem,
|
||||
) : // Match for lines for the runner in syslog.
|
||||
bool usingCISystem, {
|
||||
bool forceXcodeDebug = false,
|
||||
}) : // Match for lines for the runner in syslog.
|
||||
//
|
||||
// iOS 9 format: Runner[297] <Notice>:
|
||||
// iOS 10 format: Runner(Flutter)[297] <Notice>:
|
||||
_runnerLineRegex = RegExp(appName + r'(\(Flutter\))?\[[\d]+\] <[A-Za-z]+>: '),
|
||||
_usingCISystem = usingCISystem;
|
||||
_usingCISystem = usingCISystem,
|
||||
_forceXcodeDebug = forceXcodeDebug;
|
||||
|
||||
/// Create a new [IOSDeviceLogReader].
|
||||
factory IOSDeviceLogReader.create({
|
||||
@ -779,8 +1007,11 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
||||
device.majorSdkVersion,
|
||||
device.id,
|
||||
device.name,
|
||||
device.isWirelesslyConnected,
|
||||
device.isCoreDevice,
|
||||
appName,
|
||||
usingCISystem,
|
||||
forceXcodeDebug: device._platform.environment['FORCE_XCODE_DEBUG']?.toLowerCase() == 'true',
|
||||
);
|
||||
}
|
||||
|
||||
@ -790,6 +1021,8 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
||||
bool useSyslog = true,
|
||||
bool usingCISystem = false,
|
||||
int? majorSdkVersion,
|
||||
bool isWirelesslyConnected = false,
|
||||
bool isCoreDevice = false,
|
||||
}) {
|
||||
final int sdkVersion;
|
||||
if (majorSdkVersion != null) {
|
||||
@ -798,16 +1031,22 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
||||
sdkVersion = useSyslog ? 12 : 13;
|
||||
}
|
||||
return IOSDeviceLogReader._(
|
||||
iMobileDevice, sdkVersion, '1234', 'test', 'Runner', usingCISystem);
|
||||
iMobileDevice, sdkVersion, '1234', 'test', isWirelesslyConnected, isCoreDevice, 'Runner', usingCISystem);
|
||||
}
|
||||
|
||||
@override
|
||||
final String name;
|
||||
final int _majorSdkVersion;
|
||||
final String _deviceId;
|
||||
final bool _isWirelesslyConnected;
|
||||
final bool _isCoreDevice;
|
||||
final IMobileDevice _iMobileDevice;
|
||||
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.
|
||||
RegExp _runnerLineRegex;
|
||||
|
||||
@ -845,16 +1084,13 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
||||
/// is true.
|
||||
final List<String> _streamFlutterMessages = <String>[];
|
||||
|
||||
/// When using both `idevicesyslog` and `ios-deploy`, exclude logs with the
|
||||
/// "flutter:" prefix if they have already been added to the stream. This is
|
||||
/// to prevent duplicates from being printed.
|
||||
///
|
||||
/// If a message does not have the prefix, exclude it if the message's
|
||||
/// 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.
|
||||
/// There are three potential logging sources: `idevicesyslog`, `ios-deploy`,
|
||||
/// and Unified Logging (Dart VM). When using more than one of these logging
|
||||
/// 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
|
||||
/// being printed.
|
||||
bool _excludeLog(String message, IOSDeviceLogSource source) {
|
||||
if (!useBothLogDeviceReaders) {
|
||||
if (!usingMultipleLoggingSources) {
|
||||
return false;
|
||||
}
|
||||
if (message.startsWith('flutter:')) {
|
||||
@ -862,7 +1098,12 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
||||
return true;
|
||||
}
|
||||
_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 false;
|
||||
@ -887,12 +1128,114 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
||||
|
||||
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.
|
||||
///
|
||||
/// Only send logs to stream if [_iosDeployDebugger] is null or
|
||||
/// the [_iosDeployDebugger] debugger is not attached.
|
||||
Future<void> _listenToUnifiedLoggingEvents(FlutterVmService connectedVmService) async {
|
||||
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
|
||||
if (!_shouldListenForUnifiedLoggingEvents) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@ -909,7 +1252,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
||||
}
|
||||
|
||||
void logMessage(vm_service.Event event) {
|
||||
if (_iosDeployDebugger != null && _iosDeployDebugger!.debuggerAttached) {
|
||||
if (!useUnifiedLogging) {
|
||||
// Prefer the more complete logs from the attached debugger.
|
||||
return;
|
||||
}
|
||||
@ -931,7 +1274,7 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
||||
/// Send messages from ios-deploy debugger stream to device log reader stream.
|
||||
set debuggerStream(IOSDeployDebugger? debugger) {
|
||||
// Logging is gathered from syslog on iOS earlier than 13.
|
||||
if (_majorSdkVersion < minimumUniversalLoggingSdkVersion) {
|
||||
if (!useIOSDeployLogging) {
|
||||
return;
|
||||
}
|
||||
_iosDeployDebugger = debugger;
|
||||
@ -954,22 +1297,10 @@ class IOSDeviceLogReader extends DeviceLogReader {
|
||||
// Strip off the logging metadata (leave the category), or just echo the 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
|
||||
/// prior to 13 or if [useBothLogDeviceReaders] is true.
|
||||
void _listenToSysLog() {
|
||||
// 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 so only use syslogs for iOS versions prior
|
||||
// to 13 unless [useBothLogDeviceReaders] is true.
|
||||
if (!useBothLogDeviceReaders && _majorSdkVersion >= minimumUniversalLoggingSdkVersion) {
|
||||
if (!useSyslogLogging) {
|
||||
return;
|
||||
}
|
||||
_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.
|
||||
// This is to allow ios-deploy to be the source of authority to close
|
||||
// the stream.
|
||||
if (useBothLogDeviceReaders && debuggerStream != null) {
|
||||
if (useSyslogLogging && useIOSDeployLogging && debuggerStream != null) {
|
||||
return;
|
||||
}
|
||||
linesController.close();
|
||||
|
@ -132,6 +132,7 @@ Future<XcodeBuildResult> buildXcodeProject({
|
||||
DarwinArch? activeArch,
|
||||
bool codesign = true,
|
||||
String? deviceID,
|
||||
bool isCoreDevice = false,
|
||||
bool configOnly = false,
|
||||
XcodeBuildAction buildAction = XcodeBuildAction.build,
|
||||
}) async {
|
||||
@ -240,6 +241,7 @@ Future<XcodeBuildResult> buildXcodeProject({
|
||||
project: project,
|
||||
targetOverride: targetOverride,
|
||||
buildInfo: buildInfo,
|
||||
usingCoreDevice: isCoreDevice,
|
||||
);
|
||||
await processPodsIfNeeded(project.ios, getIosBuildDirectory(), buildInfo.mode);
|
||||
if (configOnly) {
|
||||
|
@ -35,6 +35,7 @@ Future<void> updateGeneratedXcodeProperties({
|
||||
String? targetOverride,
|
||||
bool useMacOSConfig = false,
|
||||
String? buildDirOverride,
|
||||
bool usingCoreDevice = false,
|
||||
}) async {
|
||||
final List<String> xcodeBuildSettings = await _xcodeBuildSettingsLines(
|
||||
project: project,
|
||||
@ -42,6 +43,7 @@ Future<void> updateGeneratedXcodeProperties({
|
||||
targetOverride: targetOverride,
|
||||
useMacOSConfig: useMacOSConfig,
|
||||
buildDirOverride: buildDirOverride,
|
||||
usingCoreDevice: usingCoreDevice,
|
||||
);
|
||||
|
||||
_updateGeneratedXcodePropertiesFile(
|
||||
@ -143,6 +145,7 @@ Future<List<String>> _xcodeBuildSettingsLines({
|
||||
String? targetOverride,
|
||||
bool useMacOSConfig = false,
|
||||
String? buildDirOverride,
|
||||
bool usingCoreDevice = false,
|
||||
}) async {
|
||||
final List<String> xcodeBuildSettings = <String>[];
|
||||
|
||||
@ -170,6 +173,12 @@ Future<List<String>> _xcodeBuildSettingsLines({
|
||||
final String buildNumber = parsedBuildNumber(manifest: project.manifest, buildInfo: buildInfo) ?? '1';
|
||||
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;
|
||||
if (localEngineInfo != null) {
|
||||
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 '../artifacts.dart';
|
||||
import '../base/file_system.dart';
|
||||
import '../base/io.dart';
|
||||
import '../base/logger.dart';
|
||||
import '../base/platform.dart';
|
||||
@ -18,10 +19,12 @@ import '../cache.dart';
|
||||
import '../convert.dart';
|
||||
import '../device.dart';
|
||||
import '../globals.dart' as globals;
|
||||
import '../ios/core_devices.dart';
|
||||
import '../ios/devices.dart';
|
||||
import '../ios/ios_deploy.dart';
|
||||
import '../ios/iproxy.dart';
|
||||
import '../ios/mac.dart';
|
||||
import '../ios/xcode_debug.dart';
|
||||
import '../reporting/reporting.dart';
|
||||
import 'xcode.dart';
|
||||
|
||||
@ -65,6 +68,10 @@ class XCDevice {
|
||||
required Xcode xcode,
|
||||
required Platform platform,
|
||||
required IProxy iproxy,
|
||||
required FileSystem fileSystem,
|
||||
@visibleForTesting
|
||||
IOSCoreDeviceControl? coreDeviceControl,
|
||||
XcodeDebug? xcodeDebug,
|
||||
}) : _processUtils = ProcessUtils(logger: logger, processManager: processManager),
|
||||
_logger = logger,
|
||||
_iMobileDevice = IMobileDevice(
|
||||
@ -80,6 +87,18 @@ class XCDevice {
|
||||
platform: platform,
|
||||
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,
|
||||
_xcode = xcode {
|
||||
|
||||
@ -99,6 +118,8 @@ class XCDevice {
|
||||
final IOSDeploy _iosDeploy;
|
||||
final Xcode _xcode;
|
||||
final IProxy _iProxy;
|
||||
final IOSCoreDeviceControl _coreDeviceControl;
|
||||
final XcodeDebug _xcodeDebug;
|
||||
|
||||
List<Object>? _cachedListResults;
|
||||
|
||||
@ -457,6 +478,17 @@ class XCDevice {
|
||||
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,
|
||||
@ -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(
|
||||
identifier,
|
||||
name: name,
|
||||
cpuArchitecture: _cpuArchitecture(device),
|
||||
connectionInterface: _interfaceType(device),
|
||||
connectionInterface: connectionInterface,
|
||||
isConnected: isConnected,
|
||||
sdkVersion: sdkVersionString,
|
||||
iProxy: _iProxy,
|
||||
@ -577,8 +625,11 @@ class XCDevice {
|
||||
logger: _logger,
|
||||
iosDeploy: _iosDeploy,
|
||||
iMobileDevice: _iMobileDevice,
|
||||
coreDeviceControl: _coreDeviceControl,
|
||||
xcodeDebug: _xcodeDebug,
|
||||
platform: globals.platform,
|
||||
devModeEnabled: devModeEnabled,
|
||||
isCoreDevice: coreDevice != null,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -14,8 +14,10 @@ import '../base/io.dart';
|
||||
import '../base/logger.dart';
|
||||
import '../base/platform.dart';
|
||||
import '../base/process.dart';
|
||||
import '../base/user_messages.dart';
|
||||
import '../base/version.dart';
|
||||
import '../build_info.dart';
|
||||
import '../cache.dart';
|
||||
import '../ios/xcodeproj.dart';
|
||||
|
||||
Version get xcodeRequiredVersion => Version(14, null, null);
|
||||
@ -44,9 +46,13 @@ class Xcode {
|
||||
required Logger logger,
|
||||
required FileSystem fileSystem,
|
||||
required XcodeProjectInterpreter xcodeProjectInterpreter,
|
||||
required UserMessages userMessages,
|
||||
String? flutterRoot,
|
||||
}) : _platform = platform,
|
||||
_fileSystem = fileSystem,
|
||||
_xcodeProjectInterpreter = xcodeProjectInterpreter,
|
||||
_userMessage = userMessages,
|
||||
_flutterRoot = flutterRoot,
|
||||
_processUtils =
|
||||
ProcessUtils(logger: logger, processManager: processManager),
|
||||
_logger = logger;
|
||||
@ -61,6 +67,7 @@ class Xcode {
|
||||
XcodeProjectInterpreter? xcodeProjectInterpreter,
|
||||
Platform? platform,
|
||||
FileSystem? fileSystem,
|
||||
String? flutterRoot,
|
||||
Logger? logger,
|
||||
}) {
|
||||
platform ??= FakePlatform(
|
||||
@ -72,6 +79,8 @@ class Xcode {
|
||||
platform: platform,
|
||||
processManager: processManager,
|
||||
fileSystem: fileSystem ?? MemoryFileSystem.test(),
|
||||
userMessages: UserMessages(),
|
||||
flutterRoot: flutterRoot,
|
||||
logger: logger,
|
||||
xcodeProjectInterpreter: xcodeProjectInterpreter ?? XcodeProjectInterpreter.test(processManager: processManager),
|
||||
);
|
||||
@ -81,6 +90,8 @@ class Xcode {
|
||||
final ProcessUtils _processUtils;
|
||||
final FileSystem _fileSystem;
|
||||
final XcodeProjectInterpreter _xcodeProjectInterpreter;
|
||||
final UserMessages _userMessage;
|
||||
final String? _flutterRoot;
|
||||
final Logger _logger;
|
||||
|
||||
bool get isInstalledAndMeetsVersionCheck => _platform.isMacOS && isInstalled && isRequiredVersionSatisfactory;
|
||||
@ -101,6 +112,38 @@ class Xcode {
|
||||
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;
|
||||
|
||||
Version? get currentVersion => _xcodeProjectInterpreter.version;
|
||||
@ -150,6 +193,28 @@ class Xcode {
|
||||
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 {
|
||||
final Version? version = currentVersion;
|
||||
if (version == null) {
|
||||
|
@ -130,9 +130,9 @@ class MDnsVmServiceDiscovery {
|
||||
/// The [deviceVmservicePort] parameter must be set to specify which port
|
||||
/// to find.
|
||||
///
|
||||
/// [applicationId] and [deviceVmservicePort] are required for launch so that
|
||||
/// if multiple flutter apps are running on different devices, it will
|
||||
/// only match with the device running the desired app.
|
||||
/// [applicationId] and either [deviceVmservicePort] or [deviceName] are
|
||||
/// required for launch so that if multiple flutter apps are running on
|
||||
/// different devices, it will only match with the device running the desired app.
|
||||
///
|
||||
/// The [useDeviceIPAsHost] parameter flags whether to get the device IP
|
||||
/// 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
|
||||
/// services to become active.
|
||||
///
|
||||
/// If a Dart VM Service matching the [applicationId] and [deviceVmservicePort]
|
||||
/// cannot be found after the [timeout], it will call [throwToolExit].
|
||||
/// If a Dart VM Service matching the [applicationId] and
|
||||
/// [deviceVmservicePort]/[deviceName] cannot be found before the [timeout]
|
||||
/// is reached, it will call [throwToolExit].
|
||||
@visibleForTesting
|
||||
Future<MDnsVmServiceDiscoveryResult?> queryForLaunch({
|
||||
required String applicationId,
|
||||
required int deviceVmservicePort,
|
||||
int? deviceVmservicePort,
|
||||
String? deviceName,
|
||||
bool ipv6 = false,
|
||||
bool useDeviceIPAsHost = false,
|
||||
Duration timeout = const Duration(minutes: 10),
|
||||
}) 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(
|
||||
_client,
|
||||
applicationId: applicationId,
|
||||
deviceVmservicePort: deviceVmservicePort,
|
||||
deviceName: deviceName,
|
||||
ipv6: ipv6,
|
||||
useDeviceIPAsHost: useDeviceIPAsHost,
|
||||
timeout: timeout,
|
||||
@ -170,6 +176,7 @@ class MDnsVmServiceDiscovery {
|
||||
MDnsClient client, {
|
||||
String? applicationId,
|
||||
int? deviceVmservicePort,
|
||||
String? deviceName,
|
||||
bool ipv6 = false,
|
||||
bool useDeviceIPAsHost = false,
|
||||
Duration timeout = const Duration(minutes: 10),
|
||||
@ -178,6 +185,7 @@ class MDnsVmServiceDiscovery {
|
||||
client,
|
||||
applicationId: applicationId,
|
||||
deviceVmservicePort: deviceVmservicePort,
|
||||
deviceName: deviceName,
|
||||
ipv6: ipv6,
|
||||
useDeviceIPAsHost: useDeviceIPAsHost,
|
||||
timeout: timeout,
|
||||
@ -193,6 +201,7 @@ class MDnsVmServiceDiscovery {
|
||||
MDnsClient client, {
|
||||
String? applicationId,
|
||||
int? deviceVmservicePort,
|
||||
String? deviceName,
|
||||
bool ipv6 = false,
|
||||
bool useDeviceIPAsHost = false,
|
||||
required Duration timeout,
|
||||
@ -263,6 +272,11 @@ class MDnsVmServiceDiscovery {
|
||||
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.
|
||||
InternetAddress? ipAddress;
|
||||
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) {
|
||||
const String authCodePrefix = 'authCode=';
|
||||
final Iterable<String> matchingRecords =
|
||||
@ -354,7 +377,7 @@ class MDnsVmServiceDiscovery {
|
||||
/// When [useDeviceIPAsHost] is true, it will use the device's IP as the
|
||||
/// 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
|
||||
/// or a specific service matching [applicationId]/[deviceVmservicePort].
|
||||
/// 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
|
||||
/// host and will not forward the port.
|
||||
///
|
||||
/// Differs from `getVMServiceUriForAttach` because it only searches for a specific service.
|
||||
/// This is enforced by [applicationId] and [deviceVmservicePort] being required.
|
||||
/// Differs from [getVMServiceUriForAttach] because it only searches for a specific service.
|
||||
/// This is enforced by [applicationId] being required and using either the
|
||||
/// [deviceVmservicePort] or the [device]'s name to query.
|
||||
Future<Uri?> getVMServiceUriForLaunch(
|
||||
String applicationId,
|
||||
Device device, {
|
||||
bool usesIpv6 = false,
|
||||
int? hostVmservicePort,
|
||||
required int deviceVmservicePort,
|
||||
int? deviceVmservicePort,
|
||||
bool useDeviceIPAsHost = false,
|
||||
Duration timeout = const Duration(minutes: 10),
|
||||
}) async {
|
||||
final MDnsVmServiceDiscoveryResult? result = await queryForLaunch(
|
||||
applicationId: applicationId,
|
||||
deviceVmservicePort: deviceVmservicePort,
|
||||
deviceName: deviceVmservicePort == null ? device.name : null,
|
||||
ipv6: usesIpv6,
|
||||
useDeviceIPAsHost: useDeviceIPAsHost,
|
||||
timeout: timeout,
|
||||
|
@ -339,6 +339,15 @@
|
||||
"templates/skeleton/README.md.tmpl",
|
||||
"templates/skeleton/test/implementation_test.dart.test.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', () {
|
||||
final DebuggingOptions original = DebuggingOptions.enabled(
|
||||
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_port_forwarder.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/ios_deploy.dart';
|
||||
import 'package:flutter_tools/src/ios/ios_workflow.dart';
|
||||
import 'package:flutter_tools/src/ios/iproxy.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:test/fake.dart';
|
||||
|
||||
@ -42,6 +44,8 @@ void main() {
|
||||
late IOSDeploy iosDeploy;
|
||||
late IMobileDevice iMobileDevice;
|
||||
late FileSystem fileSystem;
|
||||
late IOSCoreDeviceControl coreDeviceControl;
|
||||
late XcodeDebug xcodeDebug;
|
||||
|
||||
setUp(() {
|
||||
final Artifacts artifacts = Artifacts.test();
|
||||
@ -61,6 +65,8 @@ void main() {
|
||||
logger: logger,
|
||||
processManager: FakeProcessManager.any(),
|
||||
);
|
||||
coreDeviceControl = FakeIOSCoreDeviceControl();
|
||||
xcodeDebug = FakeXcodeDebug();
|
||||
});
|
||||
|
||||
testWithoutContext('successfully instantiates on Mac OS', () {
|
||||
@ -72,12 +78,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
sdkVersion: '13.3',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
);
|
||||
expect(device.isSupported(), isTrue);
|
||||
});
|
||||
@ -91,11 +100,14 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
cpuArchitecture: DarwinArch.armv7,
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
);
|
||||
expect(device.isSupported(), isFalse);
|
||||
});
|
||||
@ -109,12 +121,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
sdkVersion: '1.0.0',
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
).majorSdkVersion, 1);
|
||||
expect(IOSDevice(
|
||||
'device-123',
|
||||
@ -124,12 +139,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
sdkVersion: '13.1.1',
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
).majorSdkVersion, 13);
|
||||
expect(IOSDevice(
|
||||
'device-123',
|
||||
@ -139,12 +157,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
sdkVersion: '10',
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
).majorSdkVersion, 10);
|
||||
expect(IOSDevice(
|
||||
'device-123',
|
||||
@ -154,12 +175,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
sdkVersion: '0',
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
).majorSdkVersion, 0);
|
||||
expect(IOSDevice(
|
||||
'device-123',
|
||||
@ -169,12 +193,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
sdkVersion: 'bogus',
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
).majorSdkVersion, 0);
|
||||
});
|
||||
|
||||
@ -187,12 +214,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
sdkVersion: '13.3.1',
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
).sdkVersion;
|
||||
Version expectedVersion = Version(13, 3, 1, text: '13.3.1');
|
||||
expect(sdkVersion, isNotNull);
|
||||
@ -207,12 +237,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
sdkVersion: '13.3.1 (20ADBC)',
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
).sdkVersion;
|
||||
expectedVersion = Version(13, 3, 1, text: '13.3.1 (20ADBC)');
|
||||
expect(sdkVersion, isNotNull);
|
||||
@ -227,12 +260,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
sdkVersion: '16.4.1(a) (20ADBC)',
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
).sdkVersion;
|
||||
expectedVersion = Version(16, 4, 1, text: '16.4.1(a) (20ADBC)');
|
||||
expect(sdkVersion, isNotNull);
|
||||
@ -247,12 +283,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
sdkVersion: '0',
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
).sdkVersion;
|
||||
expectedVersion = Version(0, 0, 0, text: '0');
|
||||
expect(sdkVersion, isNotNull);
|
||||
@ -267,11 +306,14 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
).sdkVersion;
|
||||
expect(sdkVersion, isNull);
|
||||
|
||||
@ -283,12 +325,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
sdkVersion: 'bogus',
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
).sdkVersion;
|
||||
expect(sdkVersion, isNull);
|
||||
});
|
||||
@ -302,12 +347,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
sdkVersion: '13.3 17C54',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
);
|
||||
|
||||
expect(await device.sdkNameAndVersion,'iOS 13.3 17C54');
|
||||
@ -322,12 +370,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
sdkVersion: '13.3',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
);
|
||||
|
||||
expect(device.supportsRuntimeMode(BuildMode.debug), true);
|
||||
@ -348,12 +399,15 @@ void main() {
|
||||
platform: platform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
sdkVersion: '13.3',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
);
|
||||
},
|
||||
throwsAssertionError,
|
||||
@ -440,12 +494,15 @@ void main() {
|
||||
platform: macPlatform,
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
name: 'iPhone 1',
|
||||
sdkVersion: '13.3',
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
);
|
||||
logReader1 = createLogReader(device, appPackage1, process1);
|
||||
logReader2 = createLogReader(device, appPackage2, process2);
|
||||
@ -471,6 +528,8 @@ void main() {
|
||||
late IOSDeploy iosDeploy;
|
||||
late IMobileDevice iMobileDevice;
|
||||
late IOSWorkflow iosWorkflow;
|
||||
late IOSCoreDeviceControl coreDeviceControl;
|
||||
late XcodeDebug xcodeDebug;
|
||||
late IOSDevice device1;
|
||||
late IOSDevice device2;
|
||||
|
||||
@ -494,6 +553,8 @@ void main() {
|
||||
processManager: fakeProcessManager,
|
||||
logger: logger,
|
||||
);
|
||||
coreDeviceControl = FakeIOSCoreDeviceControl();
|
||||
xcodeDebug = FakeXcodeDebug();
|
||||
|
||||
device1 = IOSDevice(
|
||||
'd83d5bc53967baa0ee18626ba87b6254b2ab5418',
|
||||
@ -503,12 +564,15 @@ void main() {
|
||||
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
logger: logger,
|
||||
platform: macPlatform,
|
||||
fileSystem: MemoryFileSystem.test(),
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
);
|
||||
|
||||
device2 = IOSDevice(
|
||||
@ -519,12 +583,15 @@ void main() {
|
||||
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
logger: logger,
|
||||
platform: macPlatform,
|
||||
fileSystem: MemoryFileSystem.test(),
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
);
|
||||
});
|
||||
|
||||
@ -781,6 +848,8 @@ void main() {
|
||||
late IOSDeploy iosDeploy;
|
||||
late IMobileDevice iMobileDevice;
|
||||
late IOSWorkflow iosWorkflow;
|
||||
late IOSCoreDeviceControl coreDeviceControl;
|
||||
late XcodeDebug xcodeDebug;
|
||||
late IOSDevice notConnected1;
|
||||
|
||||
setUp(() {
|
||||
@ -803,6 +872,8 @@ void main() {
|
||||
processManager: fakeProcessManager,
|
||||
logger: logger,
|
||||
);
|
||||
coreDeviceControl = FakeIOSCoreDeviceControl();
|
||||
xcodeDebug = FakeXcodeDebug();
|
||||
notConnected1 = IOSDevice(
|
||||
'00000001-0000000000000000',
|
||||
name: 'iPad',
|
||||
@ -811,12 +882,15 @@ void main() {
|
||||
iProxy: IProxy.test(logger: logger, processManager: FakeProcessManager.any()),
|
||||
iosDeploy: iosDeploy,
|
||||
iMobileDevice: iMobileDevice,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: xcodeDebug,
|
||||
logger: logger,
|
||||
platform: macPlatform,
|
||||
fileSystem: MemoryFileSystem.test(),
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: false,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: false,
|
||||
);
|
||||
});
|
||||
|
||||
@ -965,3 +1039,10 @@ class FakeProcess extends Fake implements Process {
|
||||
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/device.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/ios_deploy.dart';
|
||||
import 'package:flutter_tools/src/ios/iproxy.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/fake_process_manager.dart';
|
||||
@ -105,6 +108,28 @@ void main() {
|
||||
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 {
|
||||
final IOSApp iosApp = PrebuiltIOSApp(
|
||||
projectBundleId: 'app',
|
||||
@ -134,6 +159,28 @@ void main() {
|
||||
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', () {
|
||||
testWithoutContext('catches ProcessException from ios-deploy', () async {
|
||||
final IOSApp iosApp = PrebuiltIOSApp(
|
||||
@ -263,6 +310,28 @@ void main() {
|
||||
expect(processManager, hasNoRemainingExpectations);
|
||||
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 {
|
||||
@ -314,6 +383,8 @@ void main() {
|
||||
|
||||
expect(wasAppUninstalled, false);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
IOSDevice setUpIOSDevice({
|
||||
@ -322,6 +393,7 @@ IOSDevice setUpIOSDevice({
|
||||
Logger? logger,
|
||||
DeviceConnectionInterface? interfaceType,
|
||||
Artifacts? artifacts,
|
||||
bool isCoreDevice = false,
|
||||
}) {
|
||||
logger ??= BufferLogger.test();
|
||||
final FakePlatform platform = FakePlatform(
|
||||
@ -357,9 +429,42 @@ IOSDevice setUpIOSDevice({
|
||||
artifacts: artifacts,
|
||||
cache: cache,
|
||||
),
|
||||
coreDeviceControl: FakeIOSCoreDeviceControl(),
|
||||
xcodeDebug: FakeXcodeDebug(),
|
||||
iProxy: IProxy.test(logger: logger, processManager: processManager),
|
||||
connectionInterface: interfaceType ?? DeviceConnectionInterface.attached,
|
||||
isConnected: 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(
|
||||
iMobileDevice: IMobileDevice(
|
||||
artifacts: artifacts,
|
||||
@ -363,39 +471,13 @@ Runner(libsystem_asl.dylib)[297] <Notice>: libMobileGestalt
|
||||
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', () {
|
||||
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 {
|
||||
testWithoutContext('syslog sends flutter messages to stream when useSyslogLogging is true', () async {
|
||||
processManager.addCommand(
|
||||
FakeCommand(
|
||||
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();
|
||||
|
||||
expect(logReader.useBothLogDeviceReaders, isTrue);
|
||||
expect(logReader.useSyslogLogging, isTrue);
|
||||
expect(processManager, hasNoRemainingExpectations);
|
||||
expect(lines, <String>[
|
||||
'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(
|
||||
FakeCommand(
|
||||
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 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(lines.length, 3);
|
||||
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: 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/cache.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/ios_deploy.dart';
|
||||
import 'package:flutter_tools/src/ios/iproxy.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:test/fake.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../../src/context.dart';
|
||||
@ -94,6 +97,8 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) {
|
||||
cache: Cache.test(processManager: processManager),
|
||||
),
|
||||
iMobileDevice: IMobileDevice.test(processManager: processManager),
|
||||
coreDeviceControl: FakeIOSCoreDeviceControl(),
|
||||
xcodeDebug: FakeXcodeDebug(),
|
||||
platform: platform,
|
||||
name: 'iPhone 1',
|
||||
sdkVersion: '13.3',
|
||||
@ -102,5 +107,10 @@ IOSDevice setUpIOSDevice(FileSystem fileSystem) {
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: 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
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fake_async/fake_async.dart';
|
||||
import 'package:file/memory.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/cache.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/core_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/iproxy.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/macos/xcode.dart';
|
||||
import 'package:flutter_tools/src/project.dart';
|
||||
@ -25,6 +30,7 @@ import 'package:test/fake.dart';
|
||||
|
||||
import '../../src/common.dart';
|
||||
import '../../src/context.dart' hide FakeXcodeProjectInterpreter;
|
||||
import '../../src/fake_devices.dart';
|
||||
import '../../src/fake_process_manager.dart';
|
||||
import '../../src/fakes.dart';
|
||||
|
||||
@ -287,13 +293,363 @@ void main() {
|
||||
Xcode: () => xcode,
|
||||
}, 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('.packages').writeAsStringSync('\n');
|
||||
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);
|
||||
// This is the expected output directory.
|
||||
fileSystem.directory('build/ios/iphoneos/My Super Awesome App.app').createSync(recursive: true);
|
||||
@ -305,6 +661,9 @@ IOSDevice setUpIOSDevice({
|
||||
Logger? logger,
|
||||
ProcessManager? processManager,
|
||||
Artifacts? artifacts,
|
||||
bool isCoreDevice = false,
|
||||
IOSCoreDeviceControl? coreDeviceControl,
|
||||
FakeXcodeDebug? xcodeDebug,
|
||||
}) {
|
||||
artifacts ??= Artifacts.test();
|
||||
final Cache cache = Cache.test(
|
||||
@ -336,10 +695,13 @@ IOSDevice setUpIOSDevice({
|
||||
artifacts: artifacts,
|
||||
cache: cache,
|
||||
),
|
||||
coreDeviceControl: coreDeviceControl ?? FakeIOSCoreDeviceControl(),
|
||||
xcodeDebug: xcodeDebug ?? FakeXcodeDebug(),
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
connectionInterface: DeviceConnectionInterface.attached,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: isCoreDevice,
|
||||
);
|
||||
}
|
||||
|
||||
@ -381,3 +743,70 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete
|
||||
Duration timeout = const Duration(minutes: 1),
|
||||
}) 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:convert';
|
||||
|
||||
import 'package:fake_async/fake_async.dart';
|
||||
import 'package:file/memory.dart';
|
||||
import 'package:flutter_tools/src/artifacts.dart';
|
||||
import 'package:flutter_tools/src/base/file_system.dart';
|
||||
import 'package:flutter_tools/src/base/logger.dart';
|
||||
import 'package:flutter_tools/src/base/platform.dart';
|
||||
import 'package:flutter_tools/src/base/process.dart';
|
||||
import 'package:flutter_tools/src/base/template.dart';
|
||||
import 'package:flutter_tools/src/build_info.dart';
|
||||
import 'package:flutter_tools/src/cache.dart';
|
||||
import 'package:flutter_tools/src/device.dart';
|
||||
import 'package:flutter_tools/src/device_port_forwarder.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/ios_deploy.dart';
|
||||
import 'package:flutter_tools/src/ios/iproxy.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:test/fake.dart';
|
||||
|
||||
@ -601,6 +606,212 @@ void main() {
|
||||
expect(await device.stopApp(iosApp), false);
|
||||
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({
|
||||
@ -610,6 +821,9 @@ IOSDevice setUpIOSDevice({
|
||||
ProcessManager? processManager,
|
||||
IOSDeploy? iosDeploy,
|
||||
DeviceConnectionInterface interfaceType = DeviceConnectionInterface.attached,
|
||||
bool isCoreDevice = false,
|
||||
IOSCoreDeviceControl? coreDeviceControl,
|
||||
FakeXcodeDebug? xcodeDebug,
|
||||
}) {
|
||||
final Artifacts artifacts = Artifacts.test();
|
||||
final FakePlatform macPlatform = FakePlatform(
|
||||
@ -646,10 +860,13 @@ IOSDevice setUpIOSDevice({
|
||||
artifacts: artifacts,
|
||||
cache: cache,
|
||||
),
|
||||
coreDeviceControl: coreDeviceControl ?? FakeIOSCoreDeviceControl(),
|
||||
xcodeDebug: xcodeDebug ?? FakeXcodeDebug(),
|
||||
cpuArchitecture: DarwinArch.arm64,
|
||||
connectionInterface: interfaceType,
|
||||
isConnected: true,
|
||||
devModeEnabled: true,
|
||||
isCoreDevice: isCoreDevice,
|
||||
);
|
||||
}
|
||||
|
||||
@ -669,10 +886,88 @@ class FakeMDnsVmServiceDiscovery extends Fake implements MDnsVmServiceDiscovery
|
||||
Device device, {
|
||||
bool usesIpv6 = false,
|
||||
int? hostVmservicePort,
|
||||
required int deviceVmservicePort,
|
||||
int? deviceVmservicePort,
|
||||
bool useDeviceIPAsHost = false,
|
||||
Duration timeout = Duration.zero,
|
||||
}) async {
|
||||
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',
|
||||
);
|
||||
});
|
||||
|
||||
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 'package:file/memory.dart';
|
||||
import 'package:flutter_tools/src/artifacts.dart';
|
||||
import 'package:flutter_tools/src/base/io.dart' show ProcessException;
|
||||
import 'package:flutter_tools/src/base/logger.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/build_info.dart';
|
||||
import 'package:flutter_tools/src/cache.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/iproxy.dart';
|
||||
import 'package:flutter_tools/src/ios/xcode_debug.dart';
|
||||
import 'package:flutter_tools/src/ios/xcodeproj.dart';
|
||||
import 'package:flutter_tools/src/macos/xcdevice.dart';
|
||||
import 'package:flutter_tools/src/macos/xcode.dart';
|
||||
@ -75,7 +79,7 @@ void main() {
|
||||
expect(fakeProcessManager, hasNoRemainingExpectations);
|
||||
});
|
||||
|
||||
testWithoutContext('isSimctlInstalled is true when simctl list fails', () {
|
||||
testWithoutContext('isSimctlInstalled is false when simctl list fails', () {
|
||||
fakeProcessManager.addCommand(
|
||||
const FakeCommand(
|
||||
command: <String>[
|
||||
@ -97,6 +101,156 @@ void main() {
|
||||
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', () {
|
||||
late Xcode xcode;
|
||||
late BufferLogger logger;
|
||||
@ -339,6 +493,7 @@ void main() {
|
||||
group('xcdevice not installed', () {
|
||||
late XCDevice xcdevice;
|
||||
late Xcode xcode;
|
||||
late MemoryFileSystem fileSystem;
|
||||
|
||||
setUp(() {
|
||||
xcode = Xcode.test(
|
||||
@ -348,6 +503,7 @@ void main() {
|
||||
version: null, // Not installed.
|
||||
),
|
||||
);
|
||||
fileSystem = MemoryFileSystem.test();
|
||||
xcdevice = XCDevice(
|
||||
processManager: fakeProcessManager,
|
||||
logger: logger,
|
||||
@ -356,6 +512,9 @@ void main() {
|
||||
artifacts: Artifacts.test(),
|
||||
cache: Cache.test(processManager: FakeProcessManager.any()),
|
||||
iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
|
||||
fileSystem: fileSystem,
|
||||
coreDeviceControl: FakeIOSCoreDeviceControl(),
|
||||
xcodeDebug: FakeXcodeDebug(),
|
||||
);
|
||||
});
|
||||
|
||||
@ -373,9 +532,13 @@ void main() {
|
||||
group('xcdevice', () {
|
||||
late XCDevice xcdevice;
|
||||
late Xcode xcode;
|
||||
late MemoryFileSystem fileSystem;
|
||||
late FakeIOSCoreDeviceControl coreDeviceControl;
|
||||
|
||||
setUp(() {
|
||||
xcode = Xcode.test(processManager: FakeProcessManager.any());
|
||||
fileSystem = MemoryFileSystem.test();
|
||||
coreDeviceControl = FakeIOSCoreDeviceControl();
|
||||
xcdevice = XCDevice(
|
||||
processManager: fakeProcessManager,
|
||||
logger: logger,
|
||||
@ -384,6 +547,9 @@ void main() {
|
||||
artifacts: Artifacts.test(),
|
||||
cache: Cache.test(processManager: FakeProcessManager.any()),
|
||||
iproxy: IProxy.test(logger: logger, processManager: fakeProcessManager),
|
||||
fileSystem: fileSystem,
|
||||
coreDeviceControl: coreDeviceControl,
|
||||
xcodeDebug: FakeXcodeDebug(),
|
||||
);
|
||||
});
|
||||
|
||||
@ -1117,6 +1283,176 @@ void main() {
|
||||
}, overrides: <Type, Generator>{
|
||||
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', () {
|
||||
@ -1312,3 +1648,41 @@ class FakeXcodeProjectInterpreter extends Fake implements XcodeProjectInterprete
|
||||
@override
|
||||
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', () {
|
||||
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 {
|
||||
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.'),
|
||||
);
|
||||
});
|
||||
|
||||
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 {
|
||||
@ -895,6 +994,11 @@ class FakeMDnsClient extends Fake implements MDnsClient {
|
||||
// Until we fix that, we have to also ignore related lints here.
|
||||
// ignore: avoid_implementing_value_types
|
||||
class FakeIOSDevice extends Fake implements IOSDevice {
|
||||
FakeIOSDevice({this.name = 'iPhone'});
|
||||
|
||||
@override
|
||||
final String name;
|
||||
|
||||
@override
|
||||
Future<TargetPlatform> get targetPlatform async => TargetPlatform.ios;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user