[CP] Wait for CONFIGURATION_BUILD_DIR to update when debugging with Xcode (#135609)
Original PR: https://github.com/flutter/flutter/pull/135444
diff --git a/packages/flutter_tools/bin/xcode_debug.js b/packages/flutter_tools/bin/xcode_debug.js
index 25f16a2..611ba1b 100644
--- a/packages/flutter_tools/bin/xcode_debug.js
+++ b/packages/flutter_tools/bin/xcode_debug.js
@@ -61,6 +61,11 @@
this.xcodePath = this.validatedStringArgument('--xcode-path', parsedArguments['--xcode-path']);
this.projectPath = this.validatedStringArgument('--project-path', parsedArguments['--project-path']);
+ this.projectName = this.validatedStringArgument('--project-name', parsedArguments['--project-name']);
+ this.expectedConfigurationBuildDir = this.validatedStringArgument(
+ '--expected-configuration-build-dir',
+ parsedArguments['--expected-configuration-build-dir'],
+ );
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']);
@@ -92,6 +97,45 @@
}
/**
+ * Returns map of commands to map of allowed arguments. For each command, if
+ * an argument flag is a key, than that flag is allowed for that command. If
+ * the value for the key is true, then it is required for the command.
+ *
+ * @returns {!string} Map of commands to allowed and optionally required
+ * arguments.
+ */
+ argumentSettings() {
+ return {
+ 'check-workspace-opened': {
+ '--xcode-path': true,
+ '--project-path': true,
+ '--workspace-path': true,
+ '--verbose': false,
+ },
+ 'debug': {
+ '--xcode-path': true,
+ '--project-path': true,
+ '--workspace-path': true,
+ '--project-name': true,
+ '--expected-configuration-build-dir': false,
+ '--device-id': true,
+ '--scheme': true,
+ '--skip-building': true,
+ '--launch-args': true,
+ '--verbose': false,
+ },
+ 'stop': {
+ '--xcode-path': true,
+ '--project-path': true,
+ '--workspace-path': true,
+ '--close-window': true,
+ '--prompt-to-save': true,
+ '--verbose': false,
+ },
+ };
+ }
+
+ /**
* Validates the flag is allowed for the current command.
*
* @param {!string} flag
@@ -101,27 +145,7 @@
* 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;
+ const isAllowed = this.argumentSettings()[this.command].hasOwnProperty(flag);
if (isAllowed === false && (value != null && value !== '')) {
throw `The flag ${flag} is not allowed for the command ${this.command}.`;
}
@@ -129,6 +153,21 @@
}
/**
+ * Validates required flag has a value.
+ *
+ * @param {!string} flag
+ * @param {?string} value
+ * @throws Will throw an error if the flag is required for the current
+ * command and the value is not null, undefined, or empty.
+ */
+ validateRequiredArgument(flag, value) {
+ const isRequired = this.argumentSettings()[this.command][flag] === true;
+ if (isRequired === true && (value == null || value === '')) {
+ throw `Missing value for ${flag}`;
+ }
+ }
+
+ /**
* Parses the command line arguments into an object.
*
* @param {!Array<string>} args List of arguments passed from the command line.
@@ -182,9 +221,7 @@
if (this.isArgumentAllowed(flag, value) === false) {
return null;
}
- if (value == null || value === '') {
- throw `Missing value for ${flag}`;
- }
+ this.validateRequiredArgument(flag, value);
return value;
}
@@ -226,9 +263,7 @@
if (this.isArgumentAllowed(flag, value) === false) {
return null;
}
- if (value == null || value === '') {
- throw `Missing value for ${flag}`;
- }
+ this.validateRequiredArgument(flag, value);
try {
return JSON.parse(value);
} catch (e) {
@@ -347,6 +382,15 @@
return new FunctionResult(null, destinationResult.error)
}
+ // If expectedConfigurationBuildDir is available, ensure that it matches the
+ // build settings.
+ if (args.expectedConfigurationBuildDir != null && args.expectedConfigurationBuildDir !== '') {
+ const updateResult = waitForConfigurationBuildDirToUpdate(targetWorkspace, args);
+ if (updateResult.error != null) {
+ return new FunctionResult(null, updateResult.error);
+ }
+ }
+
try {
// Documentation from the Xcode Script Editor dictionary indicates that the
// `debug` function has a parameter called `runDestinationSpecifier` which
@@ -528,3 +572,92 @@
}
return new FunctionResult(null, null);
}
+
+/**
+ * Gets resolved build setting for CONFIGURATION_BUILD_DIR and waits until its
+ * value matches the `--expected-configuration-build-dir` argument. Waits up to
+ * 2 minutes.
+ *
+ * @param {!WorkspaceDocument} targetWorkspace A `WorkspaceDocument` (Xcode Mac
+ * Scripting class).
+ * @param {!CommandArguments} args
+ * @returns {!FunctionResult} Always returns null as the `result`.
+ */
+function waitForConfigurationBuildDirToUpdate(targetWorkspace, args) {
+ // Get the project
+ let project;
+ try {
+ project = targetWorkspace.projects().find(x => x.name() == args.projectName);
+ } catch (e) {
+ return new FunctionResult(null, `Failed to find project ${args.projectName}: ${e}`);
+ }
+ if (project == null) {
+ return new FunctionResult(null, `Failed to find project ${args.projectName}.`);
+ }
+
+ // Get the target
+ let target;
+ try {
+ // The target is probably named the same as the project, but if not, just use the first.
+ const targets = project.targets();
+ target = targets.find(x => x.name() == args.projectName);
+ if (target == null && targets.length > 0) {
+ target = targets[0];
+ if (args.verbose) {
+ console.log(`Failed to find target named ${args.projectName}, picking first target: ${target.name()}.`);
+ }
+ }
+ } catch (e) {
+ return new FunctionResult(null, `Failed to find target: ${e}`);
+ }
+ if (target == null) {
+ return new FunctionResult(null, `Failed to find target.`);
+ }
+
+ try {
+ // Use the first build configuration (Debug). Any should do since they all
+ // include Generated.xcconfig.
+ const buildConfig = target.buildConfigurations()[0];
+ const buildSettings = buildConfig.resolvedBuildSettings().reverse();
+
+ // CONFIGURATION_BUILD_DIR is often at (reverse) index 225 for Xcode
+ // projects, so check there first. If it's not there, search the build
+ // settings (which can be a little slow).
+ const defaultIndex = 225;
+ let configurationBuildDirSettings;
+ if (buildSettings[defaultIndex] != null && buildSettings[defaultIndex].name() === 'CONFIGURATION_BUILD_DIR') {
+ configurationBuildDirSettings = buildSettings[defaultIndex];
+ } else {
+ configurationBuildDirSettings = buildSettings.find(x => x.name() === 'CONFIGURATION_BUILD_DIR');
+ }
+
+ if (configurationBuildDirSettings == null) {
+ // This should not happen, even if it's not set by Flutter, there should
+ // always be a resolved build setting for CONFIGURATION_BUILD_DIR.
+ return new FunctionResult(null, `Unable to find CONFIGURATION_BUILD_DIR.`);
+ }
+
+ // Wait up to 2 minutes for the CONFIGURATION_BUILD_DIR to update to the
+ // expected value.
+ const checkFrequencyInSeconds = 0.5;
+ const maxWaitInSeconds = 2 * 60; // 2 minutes
+ const verboseLogInterval = 10 * (1 / checkFrequencyInSeconds);
+ const iterations = maxWaitInSeconds * (1 / checkFrequencyInSeconds);
+ for (let i = 0; i < iterations; i++) {
+ const verbose = args.verbose && i % verboseLogInterval === 0;
+
+ const configurationBuildDir = configurationBuildDirSettings.value();
+ if (configurationBuildDir === args.expectedConfigurationBuildDir) {
+ console.log(`CONFIGURATION_BUILD_DIR: ${configurationBuildDir}`);
+ return new FunctionResult(null, null);
+ }
+ if (verbose) {
+ console.log(`Current CONFIGURATION_BUILD_DIR: ${configurationBuildDir} while expecting ${args.expectedConfigurationBuildDir}`);
+ }
+ delay(checkFrequencyInSeconds);
+ }
+ return new FunctionResult(null, 'Timed out waiting for CONFIGURATION_BUILD_DIR to update.');
+ } catch (e) {
+ return new FunctionResult(null, `Failed to get CONFIGURATION_BUILD_DIR: ${e}`);
+ }
+}
diff --git a/packages/flutter_tools/lib/src/ios/devices.dart b/packages/flutter_tools/lib/src/ios/devices.dart
index 0316576..ca6e594 100644
--- a/packages/flutter_tools/lib/src/ios/devices.dart
+++ b/packages/flutter_tools/lib/src/ios/devices.dart
@@ -721,6 +721,18 @@
return LaunchResult.failed();
} finally {
startAppStatus.stop();
+
+ if ((isCoreDevice || forceXcodeDebugWorkflow) && debuggingOptions.debuggingEnabled && package is BuildableIOSApp) {
+ // When debugging via Xcode, after the app launches, reset the Generated
+ // settings to not include the custom configuration build directory.
+ // This is to prevent confusion if the project is later ran via Xcode
+ // rather than the Flutter CLI.
+ await updateGeneratedXcodeProperties(
+ project: FlutterProject.current(),
+ buildInfo: debuggingOptions.buildInfo,
+ targetOverride: mainPath,
+ );
+ }
}
}
@@ -818,6 +830,8 @@
scheme: scheme,
xcodeProject: project.xcodeProject,
xcodeWorkspace: project.xcodeWorkspace!,
+ hostAppProjectName: project.hostAppProjectName,
+ expectedConfigurationBuildDir: bundle.parent.absolute.path,
verboseLogging: _logger.isVerbose,
);
} else {
@@ -839,18 +853,6 @@
shutdownHooks.addShutdownHook(() => _xcodeDebug.exit(force: true));
}
- if (package is BuildableIOSApp) {
- // After automating Xcode, reset the Generated settings to not include
- // the custom configuration build directory. This is to prevent
- // confusion if the project is later ran via Xcode rather than the
- // Flutter CLI.
- await updateGeneratedXcodeProperties(
- project: flutterProject,
- buildInfo: debuggingOptions.buildInfo,
- targetOverride: mainPath,
- );
- }
-
return debugSuccess;
}
}
diff --git a/packages/flutter_tools/lib/src/ios/xcode_debug.dart b/packages/flutter_tools/lib/src/ios/xcode_debug.dart
index e1b5036..563ec9d 100644
--- a/packages/flutter_tools/lib/src/ios/xcode_debug.dart
+++ b/packages/flutter_tools/lib/src/ios/xcode_debug.dart
@@ -85,6 +85,13 @@
project.xcodeProject.path,
'--workspace-path',
project.xcodeWorkspace.path,
+ '--project-name',
+ project.hostAppProjectName,
+ if (project.expectedConfigurationBuildDir != null)
+ ...<String>[
+ '--expected-configuration-build-dir',
+ project.expectedConfigurationBuildDir!,
+ ],
'--device-id',
deviceId,
'--scheme',
@@ -310,6 +317,7 @@
_xcode.xcodeAppPath,
'-g', // Do not bring the application to the foreground.
'-j', // Launches the app hidden.
+ '-F', // Open "fresh", without restoring windows.
xcodeWorkspace.path
],
throwOnError: true,
@@ -396,6 +404,7 @@
return XcodeDebugProject(
scheme: 'Runner',
+ hostAppProjectName: 'Runner',
xcodeProject: tempXcodeProject.childDirectory('Runner.xcodeproj'),
xcodeWorkspace: tempXcodeProject.childDirectory('Runner.xcworkspace'),
isTemporaryProject: true,
@@ -470,6 +479,8 @@
required this.scheme,
required this.xcodeWorkspace,
required this.xcodeProject,
+ required this.hostAppProjectName,
+ this.expectedConfigurationBuildDir,
this.isTemporaryProject = false,
this.verboseLogging = false,
});
@@ -477,6 +488,8 @@
final String scheme;
final Directory xcodeWorkspace;
final Directory xcodeProject;
+ final String hostAppProjectName;
+ final String? expectedConfigurationBuildDir;
final bool isTemporaryProject;
/// When [verboseLogging] is true, the xcode_debug.js script will log
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
index 00d19d1..68d78e5 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_nonprebuilt_test.dart
@@ -472,6 +472,7 @@
scheme: 'Runner',
xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
+ hostAppProjectName: 'Runner',
),
expectedDeviceId: '123',
expectedLaunchArguments: <String>['--enable-dart-profiling'],
@@ -534,6 +535,8 @@
scheme: 'Runner',
xcodeWorkspace: fileSystem.directory('/ios/Runner.xcworkspace'),
xcodeProject: fileSystem.directory('/ios/Runner.xcodeproj'),
+ hostAppProjectName: 'Runner',
+ expectedConfigurationBuildDir: '/build/ios/iphoneos',
),
expectedDeviceId: '123',
expectedLaunchArguments: <String>['--enable-dart-profiling'],
diff --git a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
index 8e8e223..9ca0ace 100644
--- a/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/ios_device_start_prebuilt_test.dart
@@ -625,6 +625,7 @@
scheme: 'Runner',
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
+ hostAppProjectName: 'Runner',
),
expectedDeviceId: '123',
expectedLaunchArguments: <String>['--enable-dart-profiling'],
@@ -669,6 +670,7 @@
scheme: 'Runner',
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
+ hostAppProjectName: 'Runner',
),
expectedDeviceId: '123',
expectedLaunchArguments: <String>['--enable-dart-profiling'],
@@ -729,6 +731,7 @@
scheme: 'Runner',
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
+ hostAppProjectName: 'Runner',
),
expectedDeviceId: '123',
expectedLaunchArguments: <String>['--enable-dart-profiling'],
@@ -781,6 +784,7 @@
scheme: 'Runner',
xcodeWorkspace: temporaryXcodeProjectDirectory.childDirectory('Runner.xcworkspace'),
xcodeProject: temporaryXcodeProjectDirectory.childDirectory('Runner.xcodeproj'),
+ hostAppProjectName: 'Runner',
),
expectedDeviceId: '123',
expectedLaunchArguments: <String>['--enable-dart-profiling'],
diff --git a/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart b/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart
index cbd2416..0fe3a8a 100644
--- a/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart
+++ b/packages/flutter_tools/test/general.shard/ios/xcode_debug_test.dart
@@ -56,10 +56,11 @@
scheme: 'Runner',
xcodeProject: xcodeproj,
xcodeWorkspace: xcworkspace,
+ hostAppProjectName: 'Runner',
);
});
- testWithoutContext('succeeds in opening and debugging with launch options and verbose logging', () async {
+ testWithoutContext('succeeds in opening and debugging with launch options, expectedConfigurationBuildDir, and verbose logging', () async {
fakeProcessManager.addCommands(<FakeCommand>[
FakeCommand(
command: <String>[
@@ -88,6 +89,7 @@
pathToXcodeApp,
'-g',
'-j',
+ '-F',
xcworkspace.path
],
),
@@ -105,6 +107,10 @@
project.xcodeProject.path,
'--workspace-path',
project.xcodeWorkspace.path,
+ '--project-name',
+ project.hostAppProjectName,
+ '--expected-configuration-build-dir',
+ '/build/ios/iphoneos',
'--device-id',
deviceId,
'--scheme',
@@ -131,6 +137,8 @@
scheme: 'Runner',
xcodeProject: xcodeproj,
xcodeWorkspace: xcworkspace,
+ hostAppProjectName: 'Runner',
+ expectedConfigurationBuildDir: '/build/ios/iphoneos',
verboseLogging: true,
);
@@ -150,7 +158,7 @@
expect(status, true);
});
- testWithoutContext('succeeds in opening and debugging without launch options and verbose logging', () async {
+ testWithoutContext('succeeds in opening and debugging without launch options, expectedConfigurationBuildDir, and verbose logging', () async {
fakeProcessManager.addCommands(<FakeCommand>[
FakeCommand(
command: <String>[
@@ -178,6 +186,7 @@
pathToXcodeApp,
'-g',
'-j',
+ '-F',
xcworkspace.path
],
),
@@ -195,6 +204,8 @@
project.xcodeProject.path,
'--workspace-path',
project.xcodeWorkspace.path,
+ '--project-name',
+ project.hostAppProjectName,
'--device-id',
deviceId,
'--scheme',
@@ -257,6 +268,7 @@
pathToXcodeApp,
'-g',
'-j',
+ '-F',
xcworkspace.path
],
exception: ProcessException(
@@ -266,6 +278,7 @@
'/non_existant_path',
'-g',
'-j',
+ '-F',
xcworkspace.path,
],
'The application /non_existant_path cannot be opened for an unexpected reason',
@@ -332,6 +345,8 @@
project.xcodeProject.path,
'--workspace-path',
project.xcodeWorkspace.path,
+ '--project-name',
+ project.hostAppProjectName,
'--device-id',
deviceId,
'--scheme',
@@ -401,6 +416,8 @@
project.xcodeProject.path,
'--workspace-path',
project.xcodeWorkspace.path,
+ '--project-name',
+ project.hostAppProjectName,
'--device-id',
deviceId,
'--scheme',
@@ -474,6 +491,8 @@
project.xcodeProject.path,
'--workspace-path',
project.xcodeWorkspace.path,
+ '--project-name',
+ project.hostAppProjectName,
'--device-id',
deviceId,
'--scheme',
@@ -547,6 +566,8 @@
project.xcodeProject.path,
'--workspace-path',
project.xcodeWorkspace.path,
+ '--project-name',
+ project.hostAppProjectName,
'--device-id',
deviceId,
'--scheme',
@@ -674,6 +695,7 @@
scheme: 'Runner',
xcodeProject: xcodeproj,
xcodeWorkspace: xcworkspace,
+ hostAppProjectName: 'Runner',
);
final XcodeDebug xcodeDebug = XcodeDebug(
logger: logger,
@@ -731,6 +753,7 @@
scheme: 'Runner',
xcodeProject: xcodeproj,
xcodeWorkspace: xcworkspace,
+ hostAppProjectName: 'Runner',
isTemporaryProject: true,
);
@@ -794,6 +817,7 @@
scheme: 'Runner',
xcodeProject: xcodeproj,
xcodeWorkspace: xcworkspace,
+ hostAppProjectName: 'Runner',
isTemporaryProject: true,
);
final XcodeDebug xcodeDebug = XcodeDebug(
@@ -857,6 +881,7 @@
scheme: 'Runner',
xcodeProject: xcodeproj,
xcodeWorkspace: xcworkspace,
+ hostAppProjectName: 'Runner',
);
final XcodeDebug xcodeDebug = XcodeDebug(
logger: logger,
@@ -899,6 +924,7 @@
scheme: 'Runner',
xcodeProject: xcodeproj,
xcodeWorkspace: xcworkspace,
+ hostAppProjectName: 'Runner',
isTemporaryProject: true,
);
final XcodeDebug xcodeDebug = XcodeDebug(
@@ -950,6 +976,7 @@
scheme: 'Runner',
xcodeProject: xcodeproj,
xcodeWorkspace: xcworkspace,
+ hostAppProjectName: 'Runner',
);
});