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:
Victoria Ashworth 2023-08-09 14:25:12 -05:00 committed by GitHub
parent 88ed9bd9d2
commit d631b26285
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 7609 additions and 182 deletions

View File

@ -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

View File

@ -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

View File

@ -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',
},
));
}

View File

@ -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',
},
));
}

View File

@ -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);
});

View File

@ -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(

View 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);
}

View File

@ -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,

View File

@ -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',
],

View 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;
}

View File

@ -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();

View File

@ -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) {

View File

@ -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;

View 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;
}

View File

@ -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,
);
}
}

View File

@ -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) {

View File

@ -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,

View File

@ -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"
]
}

View File

@ -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.

View File

@ -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 */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -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>

View File

@ -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>

View File

@ -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,

File diff suppressed because it is too large Load Diff

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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,
);
});
});
}

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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,
});
});
});
}

View File

@ -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;
}

View File

@ -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;